1# Copyright 2018 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Bazel rule for Android local test.""" 16 17load("//rules:acls.bzl", "acls") 18load("//rules:attrs.bzl", "attrs") 19load("//rules:common.bzl", "common") 20load("//rules:java.bzl", "java") 21load( 22 "//rules:processing_pipeline.bzl", 23 "ProviderInfo", 24 "processing_pipeline", 25) 26load("//rules:providers.bzl", "AndroidFilteredJdepsInfo") 27load("//rules:resources.bzl", "resources") 28load( 29 "//rules:utils.bzl", 30 "ANDROID_TOOLCHAIN_TYPE", 31 "compilation_mode", 32 "get_android_sdk", 33 "get_android_toolchain", 34 "log", 35 "utils", 36) 37load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 38 39JACOCOCO_CLASS = "com.google.testing.coverage.JacocoCoverageRunner" 40TEST_RUNNER_CLASS = "com.google.testing.junit.runner.BazelTestRunner" 41 42# JVM processes for android_local_test targets are typically short lived. By 43# using TieredStopAtLevel=1, aggressive JIT compilations are avoided, which is 44# more optimal for android_local_test workloads. 45DEFAULT_JIT_FLAGS = ["-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1"] 46 47# Many P99 and above android_local_test targets use a lot of memory so the default 1 GiB 48# JVM max heap size is not sufficient. Bump the max heap size to from 1 GiB -> 8 GiB. This performs 49# the best across all P% layers from profiling. 50DEFAULT_GC_FLAGS = ["-Xmx8g"] 51 52# disable class loading by default for faster classloading and consistent enviroment across 53# local and remote execution 54DEFAULT_VERIFY_FLAGS = ["-Xverify:none"] 55 56def _validations_processor(ctx, **_unused_sub_ctxs): 57 _check_src_pkg(ctx, True) 58 59def _process_manifest(ctx, java_package, **_unused_sub_ctxs): 60 manifest_ctx = None 61 manifest_values = resources.process_manifest_values( 62 ctx, 63 ctx.attr.manifest_values, 64 acls.get_min_sdk_floor(str(ctx.label)), 65 ) 66 if ctx.file.manifest == None: 67 # No manifest provided, generate one 68 manifest = ctx.actions.declare_file("_generated/" + ctx.label.name + "/AndroidManifest.xml") 69 resources.generate_dummy_manifest( 70 ctx, 71 out_manifest = manifest, 72 java_package = java_package, 73 min_sdk_version = int(manifest_values.get("minSdkVersion", 16)), # minsdk supported by robolectric framework 74 ) 75 manifest_ctx = struct(processed_manifest = manifest, processed_manifest_values = manifest_values) 76 else: 77 manifest_ctx = resources.bump_min_sdk( 78 ctx, 79 manifest = ctx.file.manifest, 80 manifest_values = ctx.attr.manifest_values, 81 floor = acls.get_min_sdk_floor(str(ctx.label)), 82 enforce_min_sdk_floor_tool = get_android_toolchain(ctx).enforce_min_sdk_floor_tool.files_to_run, 83 ) 84 85 return ProviderInfo( 86 name = "manifest_ctx", 87 value = manifest_ctx, 88 ) 89 90def _process_resources(ctx, java_package, manifest_ctx, **_unused_sub_ctxs): 91 resources_ctx = resources.package( 92 ctx, 93 deps = ctx.attr.deps, 94 manifest = manifest_ctx.processed_manifest, 95 manifest_values = manifest_ctx.processed_manifest_values, 96 resource_files = ctx.files.resource_files, 97 assets = ctx.files.assets, 98 assets_dir = ctx.attr.assets_dir, 99 resource_configs = ctx.attr.resource_configuration_filters, 100 densities = ctx.attr.densities, 101 nocompress_extensions = ctx.attr.nocompress_extensions, 102 compilation_mode = compilation_mode.get(ctx), 103 java_package = java_package, 104 shrink_resources = attrs.tristate.no, 105 aapt = get_android_toolchain(ctx).aapt2.files_to_run, 106 android_jar = get_android_sdk(ctx).android_jar, 107 busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run, 108 host_javabase = ctx.attr._host_javabase, 109 # TODO(b/140582167): Throwing on resource conflict need to be rolled 110 # out to android_local_test. 111 should_throw_on_conflict = False, 112 ) 113 114 return ProviderInfo( 115 name = "resources_ctx", 116 value = resources_ctx, 117 ) 118 119def _process_jvm(ctx, resources_ctx, **_unused_sub_ctxs): 120 deps = ( 121 ctx.attr._implicit_classpath + 122 ctx.attr.deps + 123 [get_android_toolchain(ctx).testsupport] 124 ) 125 126 if ctx.configuration.coverage_enabled: 127 deps.append(get_android_toolchain(ctx).jacocorunner) 128 java_start_class = JACOCOCO_CLASS 129 coverage_start_class = TEST_RUNNER_CLASS 130 else: 131 java_start_class = TEST_RUNNER_CLASS 132 coverage_start_class = None 133 134 java_info = java_common.add_constraints( 135 java.compile_android( 136 ctx, 137 ctx.outputs.jar, 138 ctx.actions.declare_file(ctx.label.name + "-src.jar"), 139 srcs = ctx.files.srcs, 140 resources = ctx.files.resources, 141 javac_opts = ctx.attr.javacopts, 142 r_java = resources_ctx.r_java, 143 deps = ( 144 utils.collect_providers(JavaInfo, deps) + 145 [ 146 JavaInfo( 147 output_jar = get_android_sdk(ctx).android_jar, 148 compile_jar = get_android_sdk(ctx).android_jar, 149 # The android_jar must not be compiled into the test, it 150 # will bloat the Jar with no benefit. 151 neverlink = True, 152 ), 153 ] 154 ), 155 plugins = utils.collect_providers(JavaPluginInfo, ctx.attr.plugins), 156 java_toolchain = common.get_java_toolchain(ctx), 157 ), 158 constraints = ["android"], 159 ) 160 161 # TODO(timpeut): some conformance tests require a filtered JavaInfo 162 # with no transitive_ deps. 163 providers = [java_info] 164 runfiles = [] 165 166 # Create a filtered jdeps with no resources jar. See b/129011477 for more context. 167 if java_info.outputs.jdeps != None: 168 filtered_jdeps = ctx.actions.declare_file(ctx.label.name + ".filtered.jdeps") 169 filter_jdeps(ctx, java_info.outputs.jdeps, filtered_jdeps, utils.only(resources_ctx.r_java.compile_jars.to_list())) 170 providers.append(AndroidFilteredJdepsInfo(jdeps = filtered_jdeps)) 171 runfiles.append(filtered_jdeps) 172 173 return ProviderInfo( 174 name = "jvm_ctx", 175 value = struct( 176 java_info = java_info, 177 providers = providers, 178 deps = deps, 179 java_start_class = java_start_class, 180 coverage_start_class = coverage_start_class, 181 android_properties_file = ctx.attr.robolectric_properties_file, 182 additional_jvm_flags = [], 183 ), 184 runfiles = ctx.runfiles(files = runfiles), 185 ) 186 187def _process_proto(_ctx, **_unused_sub_ctxs): 188 return ProviderInfo( 189 name = "proto_ctx", 190 value = struct( 191 proto_extension_registry_dep = depset(), 192 ), 193 ) 194 195def _process_deploy_jar(ctx, java_package, jvm_ctx, proto_ctx, resources_ctx, **_unused_sub_ctxs): 196 res_file_path = resources_ctx.validation_result.short_path 197 subs = { 198 "%android_merged_manifest%": resources_ctx.processed_manifest.short_path, 199 "%android_merged_resources%": "jar:file:" + res_file_path + "!/res", 200 "%android_merged_assets%": "jar:file:" + res_file_path + "!/assets", 201 # The native resources_ctx has the package field, whereas the starlark resources_ctx uses the java_package 202 "%android_custom_package%": getattr(resources_ctx, "package", java_package or ""), 203 "%android_resource_apk%": resources_ctx.resources_apk.short_path, 204 } 205 res_runfiles = [ 206 resources_ctx.resources_apk, 207 resources_ctx.validation_result, 208 resources_ctx.processed_manifest, 209 ] 210 211 properties_file = _genfiles_artifact(ctx, "test_config.properties") 212 properties_jar = _genfiles_artifact(ctx, "properties.jar") 213 ctx.actions.expand_template( 214 template = utils.only(get_android_toolchain(ctx).robolectric_template.files.to_list()), 215 output = properties_file, 216 substitutions = subs, 217 ) 218 _zip_file(ctx, properties_file, "com/android/tools", properties_jar) 219 properties_jar_dep = depset([properties_jar]) 220 221 runtime_deps = depset(transitive = [ 222 x.transitive_runtime_jars 223 for x in utils.collect_providers(JavaInfo, ctx.attr.runtime_deps) 224 ]) 225 android_jar_dep = depset([get_android_sdk(ctx).android_jar]) 226 out_jar_dep = depset([ctx.outputs.jar]) 227 classpath = depset( 228 transitive = [ 229 proto_ctx.proto_extension_registry_dep, 230 out_jar_dep, 231 resources_ctx.r_java.compile_jars, 232 properties_jar_dep, 233 runtime_deps, 234 android_jar_dep, 235 jvm_ctx.java_info.transitive_runtime_jars, 236 ], 237 ) 238 239 java.singlejar( 240 ctx, 241 # TODO(timpeut): investigate whether we need to filter the stub classpath as well 242 [f for f in classpath.to_list() if f.short_path.endswith(".jar")], 243 ctx.outputs.deploy_jar, 244 mnemonic = "JavaDeployJar", 245 include_build_data = True, 246 java_toolchain = common.get_java_toolchain(ctx), 247 ) 248 return ProviderInfo( 249 name = "deploy_jar_ctx", 250 value = struct( 251 classpath = classpath, 252 ), 253 runfiles = ctx.runfiles(files = res_runfiles, transitive_files = classpath), 254 ) 255 256def _preprocess_stub(ctx, **_unused_sub_ctxs): 257 javabase = ctx.attr._current_java_runtime[java_common.JavaRuntimeInfo] 258 java_executable = str(javabase.java_executable_runfiles_path) 259 java_executable_files = javabase.files 260 261 # Absolute java_executable does not require any munging 262 if java_executable.startswith("/"): 263 java_executable = "JAVABIN=" + java_executable 264 265 prefix = ctx.attr._runfiles_root_prefix[BuildSettingInfo].value 266 if not java_executable.startswith(prefix): 267 java_executable = prefix + java_executable 268 269 java_executable = "JAVABIN=${JAVABIN:-${JAVA_RUNFILES}/" + java_executable + "}" 270 271 substitutes = { 272 "%javabin%": java_executable, 273 "%load_lib%": "", 274 "%set_ASAN_OPTIONS%": "", 275 } 276 runfiles = [java_executable_files] 277 278 return ProviderInfo( 279 name = "stub_preprocess_ctx", 280 value = struct( 281 substitutes = substitutes, 282 runfiles = runfiles, 283 ), 284 ) 285 286def _process_stub(ctx, deploy_jar_ctx, jvm_ctx, stub_preprocess_ctx, **_unused_sub_ctxs): 287 runfiles = [] 288 289 merged_instr = None 290 if ctx.configuration.coverage_enabled: 291 merged_instr = ctx.actions.declare_file(ctx.label.name + "_merged_instr.jar") 292 java.singlejar( 293 ctx, 294 [f for f in deploy_jar_ctx.classpath.to_list() if f.short_path.endswith(".jar")], 295 merged_instr, 296 mnemonic = "JavaDeployJar", 297 include_build_data = True, 298 java_toolchain = common.get_java_toolchain(ctx), 299 ) 300 runfiles.append(merged_instr) 301 302 stub = ctx.actions.declare_file(ctx.label.name) 303 classpath_file = ctx.actions.declare_file(ctx.label.name + "_classpath") 304 runfiles.append(classpath_file) 305 test_class = _get_test_class(ctx) 306 if not test_class: 307 # fatal error 308 log.error("test_class could not be derived for " + str(ctx.label) + 309 ". Explicitly set test_class or move this source file to " + 310 "a java source root.") 311 312 _create_stub( 313 ctx, 314 stub_preprocess_ctx.substitutes, 315 stub, 316 classpath_file, 317 deploy_jar_ctx.classpath, 318 _get_jvm_flags(ctx, test_class, jvm_ctx.android_properties_file, jvm_ctx.additional_jvm_flags), 319 jvm_ctx.java_start_class, 320 jvm_ctx.coverage_start_class, 321 merged_instr, 322 ) 323 return ProviderInfo( 324 name = "stub_ctx", 325 value = struct( 326 stub = stub, 327 ), 328 runfiles = ctx.runfiles( 329 files = runfiles, 330 transitive_files = depset( 331 transitive = stub_preprocess_ctx.runfiles, 332 ), 333 ), 334 ) 335 336PROCESSORS = dict( 337 ValidationsProcessor = _validations_processor, 338 ManifestProcessor = _process_manifest, 339 ResourceProcessor = _process_resources, 340 JvmProcessor = _process_jvm, 341 ProtoProcessor = _process_proto, 342 DeployJarProcessor = _process_deploy_jar, 343 StubPreProcessor = _preprocess_stub, 344 StubProcessor = _process_stub, 345) 346 347def finalize( 348 ctx, 349 jvm_ctx, 350 proto_ctx, 351 providers, 352 runfiles, 353 stub_ctx, 354 validation_outputs, 355 **_unused_sub_ctxs): 356 """Creates the final providers for the rule. 357 358 Args: 359 ctx: The context. 360 jvm_ctx: ProviderInfo. The jvm ctx. 361 proto_ctx: ProviderInfo. The proto ctx. 362 providers: sequence of providers. The providers to propagate. 363 runfiles: Runfiles. The runfiles collected during processing. 364 stub_ctx: ProviderInfo. The stub ctx. 365 validation_outputs: sequence of Files. The validation outputs. 366 **_unused_sub_ctxs: Unused ProviderInfo. 367 368 Returns: 369 A struct with Android and Java legacy providers and a list of providers. 370 """ 371 runfiles = runfiles.merge(ctx.runfiles(collect_data = True)) 372 runfiles = runfiles.merge(utils.get_runfiles(ctx, jvm_ctx.deps + ctx.attr.data + ctx.attr.runtime_deps)) 373 374 providers.extend([ 375 DefaultInfo( 376 files = depset( 377 [ctx.outputs.jar, stub_ctx.stub], 378 transitive = [proto_ctx.proto_extension_registry_dep], 379 order = "preorder", 380 ), 381 executable = stub_ctx.stub, 382 runfiles = runfiles, 383 ), 384 OutputGroupInfo( 385 _validation = depset(validation_outputs), 386 ), 387 coverage_common.instrumented_files_info( 388 ctx = ctx, 389 source_attributes = ["srcs"], 390 dependency_attributes = ["deps", "runtime_deps", "data"], 391 ), 392 ]) 393 return providers 394 395_PROCESSING_PIPELINE = processing_pipeline.make_processing_pipeline( 396 processors = PROCESSORS, 397 finalize = finalize, 398) 399 400def impl(ctx): 401 java_package = java.resolve_package_from_label(ctx.label, ctx.attr.custom_package) 402 return processing_pipeline.run(ctx, java_package, _PROCESSING_PIPELINE) 403 404def _check_src_pkg(ctx, warn = True): 405 pkg = ctx.label.package 406 for attr in ctx.attr.srcs: 407 if attr.label.package != pkg: 408 msg = "Do not import %s directly. Either move the file to this package or depend on an appropriate rule there." % attr.label 409 if warn: 410 log.warn(msg) 411 else: 412 log.error(msg) 413 414def _genfiles_artifact(ctx, name): 415 return ctx.actions.declare_file( 416 "/".join([ctx.genfiles_dir.path, ctx.label.name, name]), 417 ) 418 419def _get_test_class(ctx): 420 # Use the specified test_class if set 421 if ctx.attr.test_class != "": 422 return ctx.attr.test_class 423 424 # Use a heuristic based on the rule name and the "srcs" list 425 # to determine the primary Java class. 426 expected = "/" + ctx.label.name + ".java" 427 for f in ctx.attr.srcs: 428 path = f.label.package + "/" + f.label.name 429 if path.endswith(expected): 430 return java.resolve_package(path[:-5]) 431 432 # Last resort: Use the name and package name of the target. 433 return java.resolve_package(ctx.label.package + "/" + ctx.label.name) 434 435def _create_stub( 436 ctx, 437 substitutes, 438 stub_file, 439 classpath_file, 440 runfiles, 441 jvm_flags, 442 java_start_class, 443 coverage_start_class, 444 merged_instr): 445 subs = { 446 "%needs_runfiles%": "1", 447 "%runfiles_manifest_only%": "", 448 # To avoid cracking open the depset, classpath is read from a separate 449 # file created in its own action. Needed as expand_template does not 450 # support ctx.actions.args(). 451 "%classpath%": "$(eval echo $(<%s))" % (classpath_file.short_path), 452 "%java_start_class%": java_start_class, 453 "%jvm_flags%": " ".join(jvm_flags), 454 "%workspace_prefix%": ctx.workspace_name + "/", 455 } 456 457 if coverage_start_class: 458 prefix = ctx.attr._runfiles_root_prefix[BuildSettingInfo].value 459 subs["%set_jacoco_metadata%"] = ( 460 "export JACOCO_METADATA_JAR=${JAVA_RUNFILES}/" + prefix + 461 merged_instr.short_path 462 ) 463 subs["%set_jacoco_main_class%"] = ( 464 "export JACOCO_MAIN_CLASS=" + coverage_start_class 465 ) 466 subs["%set_jacoco_java_runfiles_root%"] = ( 467 "export JACOCO_JAVA_RUNFILES_ROOT=${JAVA_RUNFILES}/" + prefix 468 ) 469 else: 470 subs["%set_jacoco_metadata%"] = "" 471 subs["%set_jacoco_main_class%"] = "" 472 subs["%set_jacoco_java_runfiles_root%"] = "" 473 474 subs.update(substitutes) 475 476 ctx.actions.expand_template( 477 template = utils.only(get_android_toolchain(ctx).java_stub.files.to_list()), 478 output = stub_file, 479 substitutions = subs, 480 is_executable = True, 481 ) 482 483 args = ctx.actions.args() 484 args.add_joined( 485 runfiles, 486 join_with = ":", 487 map_each = _get_classpath, 488 ) 489 args.set_param_file_format("multiline") 490 ctx.actions.write( 491 output = classpath_file, 492 content = args, 493 ) 494 return stub_file 495 496def _get_classpath(s): 497 return "${J3}" + s.short_path 498 499def _get_jvm_flags(ctx, main_class, robolectric_properties_path, additional_jvm_flags): 500 return [ 501 "-ea", 502 "-Dbazel.test_suite=" + main_class, 503 "-Drobolectric.offline=true", 504 "-Drobolectric-deps.properties=" + robolectric_properties_path, 505 "-Duse_framework_manifest_parser=true", 506 "-Drobolectric.logging=stdout", 507 "-Drobolectric.logging.enabled=true", 508 "-Dorg.robolectric.packagesToNotAcquire=com.google.testing.junit.runner.util", 509 ] + DEFAULT_JIT_FLAGS + DEFAULT_GC_FLAGS + DEFAULT_VERIFY_FLAGS + additional_jvm_flags + [ 510 ctx.expand_make_variables( 511 "jvm_flags", 512 ctx.expand_location(flag, ctx.attr.data), 513 {}, 514 ) 515 for flag in ctx.attr.jvm_flags 516 ] 517 518def _zip_file(ctx, f, dir_name, out_zip): 519 cmd = """ 520base=$(pwd) 521tmp_dir=$(mktemp -d) 522 523cd $tmp_dir 524mkdir -p {dir_name} 525cp $base/{f} {dir_name} 526$base/{zip_tool} -jt -X -q $base/{out_zip} {dir_name}/$(basename {f}) 527""".format( 528 zip_tool = get_android_toolchain(ctx).zip_tool.files_to_run.executable.path, 529 f = f.path, 530 dir_name = dir_name, 531 out_zip = out_zip.path, 532 ) 533 ctx.actions.run_shell( 534 command = cmd, 535 inputs = [f], 536 tools = get_android_toolchain(ctx).zip_tool.files, 537 outputs = [out_zip], 538 mnemonic = "AddToZip", 539 toolchain = ANDROID_TOOLCHAIN_TYPE, 540 ) 541 542def filter_jdeps(ctx, in_jdeps, out_jdeps, filter_suffix): 543 """Runs the JdepsFilter tool. 544 545 Args: 546 ctx: The context. 547 in_jdeps: File. The input jdeps file. 548 out_jdeps: File. The filtered jdeps output. 549 filter_suffix: File. The jdeps suffix to filter. 550 """ 551 args = ctx.actions.args() 552 args.add("--in") 553 args.add(in_jdeps.path) 554 args.add("--target") 555 args.add(filter_suffix) 556 args.add("--out") 557 args.add(out_jdeps.path) 558 ctx.actions.run( 559 inputs = [in_jdeps], 560 outputs = [out_jdeps], 561 executable = get_android_toolchain(ctx).jdeps_tool.files_to_run, 562 arguments = [args], 563 mnemonic = "JdepsFilter", 564 progress_message = "Filtering jdeps", 565 toolchain = ANDROID_TOOLCHAIN_TYPE, 566 ) 567