blob: 6a59a292c8579f1a1c3e8e1886141db74107bc90 [file] [log] [blame]
# Copyright 2018 The Bazel 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.
"""Bazel rule for Android local test."""
load("//providers:providers.bzl", "AndroidFilteredJdepsInfo")
load("//rules:attrs.bzl", "attrs")
load("//rules:common.bzl", "common")
load("//rules:java.bzl", "java")
load("//rules:min_sdk_version.bzl", "min_sdk_version")
load(
"//rules:processing_pipeline.bzl",
"ProviderInfo",
"processing_pipeline",
)
load("//rules:resources.bzl", "resources")
load(
"//rules:utils.bzl",
"ANDROID_TOOLCHAIN_TYPE",
"compilation_mode",
"get_android_sdk",
"get_android_toolchain",
"log",
"utils",
)
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
load("@rules_java//java/common:java_common.bzl", "java_common")
load("@rules_java//java/common:java_info.bzl", "JavaInfo")
load("@rules_java//java/common:java_plugin_info.bzl", "JavaPluginInfo")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
visibility(PROJECT_VISIBILITY)
JACOCOCO_CLASS = "com.google.testing.coverage.JacocoCoverageRunner"
TEST_RUNNER_CLASS = "com.google.testing.junit.runner.BazelTestRunner"
# JVM processes for android_local_test targets are typically short lived. By
# using TieredStopAtLevel=1, aggressive JIT compilations are avoided, which is
# more optimal for android_local_test workloads.
DEFAULT_JIT_FLAGS = ["-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1"]
# Many P99 and above android_local_test targets use a lot of memory so the default 1 GiB
# JVM max heap size is not sufficient. Bump the max heap size to from 1 GiB -> 8 GiB. This performs
# the best across all P% layers from profiling.
DEFAULT_GC_FLAGS = ["-Xmx8g"]
# disable class loading by default for faster classloading and consistent environment across
# local and remote execution
DEFAULT_VERIFY_FLAGS = ["-Xverify:none"]
def _validations_processor(ctx, **_unused_sub_ctxs):
_check_src_pkg(ctx, True)
def _process_manifest(ctx, java_package, **_unused_sub_ctxs):
manifest_ctx = None
manifest_values = resources.process_manifest_values(
ctx,
ctx.attr.manifest_values,
)
min_sdk = int(manifest_values.get("minSdkVersion", min_sdk_version.DEPOT_FLOOR))
if ctx.file.manifest == None:
# No manifest provided, generate one
manifest = ctx.actions.declare_file("_generated/" + ctx.label.name + "/AndroidManifest.xml")
resources.generate_dummy_manifest(
ctx,
out_manifest = manifest,
java_package = java_package,
min_sdk_version = min_sdk,
)
manifest_ctx = struct(processed_manifest = manifest, processed_manifest_values = manifest_values)
else:
manifest_ctx = resources.bump_min_sdk(
ctx,
manifest = ctx.file.manifest,
manifest_values = ctx.attr.manifest_values,
floor = min_sdk,
)
return ProviderInfo(
name = "manifest_ctx",
value = manifest_ctx,
)
def _process_resources(ctx, java_package, manifest_ctx, **_unused_sub_ctxs):
resources_ctx = resources.package(
ctx,
deps = ctx.attr.deps,
manifest = manifest_ctx.processed_manifest,
manifest_values = manifest_ctx.processed_manifest_values,
manifest_merge_order = ctx.attr._manifest_merge_order[BuildSettingInfo].value,
resource_files = ctx.files.resource_files,
assets = ctx.files.assets,
assets_dir = ctx.attr.assets_dir,
resource_configs = ctx.attr.resource_configuration_filters,
densities = ctx.attr.densities,
nocompress_extensions = ctx.attr.nocompress_extensions,
compilation_mode = compilation_mode.get(ctx),
java_package = java_package,
shrink_resources = attrs.tristate.no,
build_java_with_final_resources = True,
aapt = get_android_toolchain(ctx).aapt2.files_to_run,
android_jar = get_android_sdk(ctx).android_jar,
busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
host_javabase = ctx.attr._host_javabase,
# TODO(b/140582167): Throwing on resource conflict need to be rolled
# out to android_local_test.
should_throw_on_conflict = False,
)
return ProviderInfo(
name = "resources_ctx",
value = resources_ctx,
)
def _process_jvm(ctx, resources_ctx, **_unused_sub_ctxs):
deps = (
ctx.attr._implicit_classpath +
ctx.attr.deps +
[get_android_toolchain(ctx).testsupport]
)
if ctx.configuration.coverage_enabled:
deps.append(get_android_toolchain(ctx).jacocorunner)
java_start_class = JACOCOCO_CLASS
coverage_start_class = TEST_RUNNER_CLASS
else:
java_start_class = TEST_RUNNER_CLASS
coverage_start_class = None
java_info = java.compile_android(
ctx,
ctx.outputs.jar,
ctx.actions.declare_file(ctx.label.name + "-src.jar"),
srcs = ctx.files.srcs,
resources = ctx.files.resources,
javac_opts = ctx.attr.javacopts,
r_java = resources_ctx.r_java,
deps = (
utils.collect_providers(JavaInfo, deps) +
[
JavaInfo(
output_jar = get_android_sdk(ctx).android_jar,
compile_jar = get_android_sdk(ctx).android_jar,
# The android_jar must not be compiled into the test, it
# will bloat the Jar with no benefit.
neverlink = True,
),
]
),
plugins = utils.collect_providers(JavaPluginInfo, ctx.attr.plugins),
java_toolchain = common.get_java_toolchain(ctx),
)
if getattr(java_common, "add_constraints", None):
java_info = java_common.add_constraints(
java_info,
constraints = ["android"],
)
# TODO(timpeut): some conformance tests require a filtered JavaInfo
# with no transitive_ deps.
providers = [java_info]
runfiles = []
# Create a filtered jdeps with no resources jar. See b/129011477 for more context.
if java_info.outputs.jdeps != None:
filtered_jdeps = ctx.actions.declare_file(ctx.label.name + ".filtered.jdeps")
filter_jdeps(ctx, java_info.outputs.jdeps, filtered_jdeps, utils.only(resources_ctx.r_java.compile_jars.to_list()))
providers.append(AndroidFilteredJdepsInfo(jdeps = filtered_jdeps))
runfiles.append(filtered_jdeps)
return ProviderInfo(
name = "jvm_ctx",
value = struct(
java_info = java_info,
providers = providers,
deps = deps,
java_start_class = java_start_class,
coverage_start_class = coverage_start_class,
android_properties_file = ctx.file.robolectric_properties_file.short_path,
additional_jvm_flags = [],
),
runfiles = ctx.runfiles(files = runfiles),
)
def _process_proto(_ctx, **_unused_sub_ctxs):
return ProviderInfo(
name = "proto_ctx",
value = struct(
proto_extension_registry_dep = depset(),
),
)
def _process_deploy_jar(ctx, java_package, jvm_ctx, proto_ctx, resources_ctx, **_unused_sub_ctxs):
res_file_path = resources_ctx.validation_result.short_path
subs = {
"%android_merged_manifest%": resources_ctx.processed_manifest.short_path,
"%android_merged_resources%": "jar:file:" + res_file_path + "!/res",
"%android_merged_assets%": "jar:file:" + res_file_path + "!/assets",
# The native resources_ctx has the package field, whereas the starlark resources_ctx uses the java_package
"%android_custom_package%": getattr(resources_ctx, "package", java_package or ""),
"%android_resource_apk%": resources_ctx.resources_apk.short_path,
}
res_runfiles = [
resources_ctx.resources_apk,
resources_ctx.validation_result,
resources_ctx.processed_manifest,
]
properties_file = _genfiles_artifact(ctx, "test_config.properties")
properties_jar = _genfiles_artifact(ctx, "properties.jar")
ctx.actions.expand_template(
template = utils.only(get_android_toolchain(ctx).robolectric_template.files.to_list()),
output = properties_file,
substitutions = subs,
)
_zip_file(ctx, properties_file, "com/android/tools", properties_jar)
properties_jar_dep = depset([properties_jar])
runtime_deps = depset(transitive = [
x.transitive_runtime_jars
for x in utils.collect_providers(JavaInfo, ctx.attr.runtime_deps)
])
android_jar_dep = depset([get_android_sdk(ctx).android_jar])
out_jar_dep = depset([ctx.outputs.jar])
classpath = depset(
transitive = [
proto_ctx.proto_extension_registry_dep,
out_jar_dep,
resources_ctx.r_java.compile_jars,
properties_jar_dep,
runtime_deps,
android_jar_dep,
jvm_ctx.java_info.transitive_runtime_jars,
],
)
java.singlejar(
ctx,
# TODO(timpeut): investigate whether we need to filter the stub classpath as well
[f for f in classpath.to_list() if f.short_path.endswith(".jar")],
ctx.outputs.deploy_jar,
mnemonic = "JavaDeployJar",
include_build_data = True,
java_toolchain = common.get_java_toolchain(ctx),
)
return ProviderInfo(
name = "deploy_jar_ctx",
value = struct(
classpath = classpath,
),
runfiles = ctx.runfiles(files = res_runfiles, transitive_files = classpath),
)
def _preprocess_stub(ctx, **_unused_sub_ctxs):
javabase = ctx.attr._current_java_runtime[java_common.JavaRuntimeInfo]
java_executable = javabase.java_executable_runfiles_path
if ctx.workspace_name != "google3":
# Bazel tests need the runfiles location of the java executable, and the workspace name.
java_executable = "$(rlocation " + ctx.workspace_name + "/" + java_executable + ")"
java_executable_files = javabase.files
substitutes = {
"%javabin%": "JAVABIN=" + java_executable,
"%load_lib%": "",
"%set_ASAN_OPTIONS%": "",
}
runfiles = [java_executable_files]
return ProviderInfo(
name = "stub_preprocess_ctx",
value = struct(
substitutes = substitutes,
runfiles = runfiles,
),
)
def _process_stub(ctx, deploy_jar_ctx, jvm_ctx, stub_preprocess_ctx, **_unused_sub_ctxs):
runfiles = []
merged_instr = None
if ctx.configuration.coverage_enabled:
merged_instr = ctx.actions.declare_file(ctx.label.name + "_merged_instr.jar")
java.singlejar(
ctx,
[f for f in deploy_jar_ctx.classpath.to_list() if f.short_path.endswith(".jar")],
merged_instr,
mnemonic = "JavaDeployJar",
include_build_data = True,
java_toolchain = common.get_java_toolchain(ctx),
)
runfiles.append(merged_instr)
stub = ctx.actions.declare_file(ctx.label.name)
classpath_file = ctx.actions.declare_file(ctx.label.name + "_classpath")
runfiles.append(classpath_file)
test_class = _get_test_class(ctx)
if not test_class:
# fatal error
log.error("test_class could not be derived for " + str(ctx.label) +
". Explicitly set test_class or move this source file to " +
"a java source root.")
_create_stub(
ctx,
stub_preprocess_ctx.substitutes,
stub,
classpath_file,
deploy_jar_ctx.classpath,
_get_jvm_flags(ctx, test_class, jvm_ctx.android_properties_file, jvm_ctx.additional_jvm_flags),
jvm_ctx.java_start_class,
jvm_ctx.coverage_start_class,
merged_instr,
)
return ProviderInfo(
name = "stub_ctx",
value = struct(
stub = stub,
providers = [testing.TestEnvironment(utils.expand_make_vars(ctx, ctx.attr.env))],
),
runfiles = ctx.runfiles(
files = runfiles,
transitive_files = depset(
transitive = stub_preprocess_ctx.runfiles,
),
),
)
PROCESSORS = dict(
ValidationsProcessor = _validations_processor,
ManifestProcessor = _process_manifest,
ResourceProcessor = _process_resources,
JvmProcessor = _process_jvm,
ProtoProcessor = _process_proto,
DeployJarProcessor = _process_deploy_jar,
StubPreProcessor = _preprocess_stub,
StubProcessor = _process_stub,
)
def finalize(
ctx,
jvm_ctx,
proto_ctx,
providers,
runfiles,
stub_ctx,
validation_outputs,
**_unused_sub_ctxs):
"""Creates the final providers for the rule.
Args:
ctx: The context.
jvm_ctx: ProviderInfo. The jvm ctx.
proto_ctx: ProviderInfo. The proto ctx.
providers: sequence of providers. The providers to propagate.
runfiles: Runfiles. The runfiles collected during processing.
stub_ctx: ProviderInfo. The stub ctx.
validation_outputs: sequence of Files. The validation outputs.
**_unused_sub_ctxs: Unused ProviderInfo.
Returns:
A struct with Android and Java legacy providers and a list of providers.
"""
runfiles = runfiles.merge(ctx.runfiles(collect_data = True))
runfiles = runfiles.merge(utils.get_runfiles(ctx, jvm_ctx.deps + ctx.attr.data + ctx.attr.runtime_deps))
providers.extend([
DefaultInfo(
files = depset(
[ctx.outputs.jar, stub_ctx.stub],
transitive = [proto_ctx.proto_extension_registry_dep],
order = "preorder",
),
executable = stub_ctx.stub,
runfiles = runfiles,
),
OutputGroupInfo(
_validation = depset(validation_outputs),
),
coverage_common.instrumented_files_info(
ctx = ctx,
source_attributes = ["srcs"],
# NOTE: Associates is only applicable for OSS rules_kotlin.
dependency_attributes = ["associates", "deps", "runtime_deps", "data"],
),
])
return providers
_PROCESSING_PIPELINE = processing_pipeline.make_processing_pipeline(
processors = PROCESSORS,
finalize = finalize,
)
def impl(ctx):
java_package = java.resolve_package_from_label(ctx.label, ctx.attr.custom_package)
return processing_pipeline.run(ctx, java_package, _PROCESSING_PIPELINE)
def _check_src_pkg(ctx, warn = True):
pkg = ctx.label.package
for attr in ctx.attr.srcs:
if attr.label.package != pkg:
msg = "Do not import %s directly. Either move the file to this package or depend on an appropriate rule there." % attr.label
if warn:
log.warn(msg)
else:
log.error(msg)
def _genfiles_artifact(ctx, name):
return ctx.actions.declare_file(
"/".join([ctx.genfiles_dir.path, ctx.label.name, name]),
)
def _get_test_class(ctx):
# Use the specified test_class if set
if ctx.attr.test_class != "":
return ctx.attr.test_class
# Use a heuristic based on the rule name and the "srcs" list
# to determine the primary Java class.
expected = "/" + ctx.label.name + ".java"
for f in ctx.attr.srcs:
path = f.label.package + "/" + f.label.name
if path.endswith(expected):
return java.resolve_package(path[:-5])
# Last resort: Use the name and package name of the target.
return java.resolve_package(ctx.label.package + "/" + ctx.label.name)
def _create_stub(
ctx,
substitutes,
stub_file,
classpath_file,
runfiles,
jvm_flags,
java_start_class,
coverage_start_class,
merged_instr):
subs = {
"%needs_runfiles%": "1",
"%runfiles_manifest_only%": "",
# To avoid cracking open the depset, classpath is read from a separate
# file created in its own action. Needed as expand_template does not
# support ctx.actions.args().
"%classpath%": "$(eval echo $(<%s))" % (classpath_file.short_path),
"%java_start_class%": java_start_class,
"%jvm_flags%": " ".join(jvm_flags),
"%workspace_prefix%": ctx.workspace_name + "/",
}
if coverage_start_class:
prefix = ctx.attr._runfiles_root_prefix[BuildSettingInfo].value
subs["%set_jacoco_metadata%"] = (
"export JACOCO_METADATA_JAR=${JAVA_RUNFILES}/" + prefix +
merged_instr.short_path
)
subs["%set_jacoco_main_class%"] = (
"export JACOCO_MAIN_CLASS=" + coverage_start_class
)
subs["%set_jacoco_java_runfiles_root%"] = (
"export JACOCO_JAVA_RUNFILES_ROOT=${JAVA_RUNFILES}/" + prefix
)
else:
subs["%set_jacoco_metadata%"] = ""
subs["%set_jacoco_main_class%"] = ""
subs["%set_jacoco_java_runfiles_root%"] = ""
subs.update(substitutes)
ctx.actions.expand_template(
template = utils.only(get_android_toolchain(ctx).java_stub.files.to_list()),
output = stub_file,
substitutions = subs,
is_executable = True,
)
args = ctx.actions.args()
args.add_joined(
runfiles,
join_with = ":",
map_each = _get_classpath,
)
args.set_param_file_format("multiline")
ctx.actions.write(
output = classpath_file,
content = args,
)
return stub_file
def _get_classpath(s):
return "${J3}" + s.short_path
def _get_jvm_flags(ctx, main_class, robolectric_properties_path, additional_jvm_flags):
return [
"-ea",
"-Dbazel.test_suite=" + main_class,
"-Drobolectric.offline=true",
"-Drobolectric-deps.properties=" + robolectric_properties_path,
"-Duse_framework_manifest_parser=true",
"-Drobolectric.logging=stdout",
"-Drobolectric.logging.enabled=true",
"-Dorg.robolectric.packagesToNotAcquire=com.google.testing.junit.runner.util",
] + DEFAULT_JIT_FLAGS + DEFAULT_GC_FLAGS + DEFAULT_VERIFY_FLAGS + additional_jvm_flags + [
ctx.expand_make_variables(
"jvm_flags",
ctx.expand_location(flag, ctx.attr.data),
{},
)
for flag in ctx.attr.jvm_flags
]
def _zip_file(ctx, f, dir_name, out_zip):
cmd = """
base=$(pwd)
tmp_dir=$(mktemp -d)
cd $tmp_dir
mkdir -p {dir_name}
cp $base/{f} {dir_name}
$base/{zip_tool} -jt -X -q $base/{out_zip} {dir_name}/$(basename {f})
""".format(
zip_tool = get_android_toolchain(ctx).zip_tool.files_to_run.executable.path,
f = f.path,
dir_name = dir_name,
out_zip = out_zip.path,
)
ctx.actions.run_shell(
command = cmd,
inputs = [f],
tools = get_android_toolchain(ctx).zip_tool.files,
outputs = [out_zip],
mnemonic = "AddToZip",
toolchain = ANDROID_TOOLCHAIN_TYPE,
)
def filter_jdeps(ctx, in_jdeps, out_jdeps, filter_suffix):
"""Runs the JdepsFilter tool.
Args:
ctx: The context.
in_jdeps: File. The input jdeps file.
out_jdeps: File. The filtered jdeps output.
filter_suffix: File. The jdeps suffix to filter.
"""
args = ctx.actions.args()
args.add("--in")
args.add(in_jdeps.path)
args.add("--target")
args.add(filter_suffix)
args.add("--out")
args.add(out_jdeps.path)
ctx.actions.run(
inputs = [in_jdeps],
outputs = [out_jdeps],
executable = get_android_toolchain(ctx).jdeps_tool.files_to_run,
arguments = [args],
mnemonic = "JdepsFilter",
progress_message = "Filtering jdeps",
toolchain = ANDROID_TOOLCHAIN_TYPE,
)