blob: 23f319e7e73e32a8154abc99b6e3794ac28aa6f2 [file] [log] [blame]
# Copyright 2019 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.
"""Test rule for resource processing."""
load("//providers:providers.bzl", "StarlarkAndroidResourcesInfo")
load("//rules:attrs.bzl", "ANDROID_BINARY_ATTRS")
load("//rules:common.bzl", _common = "common")
load("//rules:java.bzl", _java = "java")
load("//rules:resources.bzl", _resources = "resources", _resources_testing = "testing")
load(
"//rules:utils.bzl",
"ANDROID_SDK_TOOLCHAIN_TYPE",
"get_android_sdk",
"get_android_toolchain",
_compilation_mode = "compilation_mode",
_utils = "utils",
)
load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
load(
"//test/utils:asserts.bzl",
_asserts = "asserts",
)
load("@rules_java//java/common:java_common.bzl", "java_common")
load(
"@bazel_skylib//lib:unittest.bzl",
"analysistest",
"asserts",
"unittest",
)
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
visibility(PROJECT_VISIBILITY)
_StarlarkResourcesTestingInfo = provider()
def _starlark_process_impl(ctx):
java_package = _java.resolve_package_from_label(ctx.label, ctx.attr.custom_package)
starlark_resources_ctx = _resources.process_starlark(
ctx,
java_package = java_package if ctx.attr.use_java_package else None,
manifest = ctx.file.manifest,
assets = ctx.files.assets,
assets_dir = ctx.attr.assets_dir,
deps = ctx.attr.deps,
exports = ctx.attr.exports,
resource_files = ctx.files.resource_files,
stamp_manifest = ctx.attr.stamp_manifest,
neverlink = ctx.attr.neverlink,
enable_data_binding = ctx.attr.enable_data_binding,
fix_resource_transitivity = ctx.attr.fix_resource_transitivity,
aapt = get_android_toolchain(ctx).aapt2.files_to_run,
android_jar = get_android_sdk(ctx).android_jar,
android_kit = get_android_toolchain(ctx).android_kit.files_to_run,
busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
java_toolchain = _common.get_java_toolchain(ctx),
host_javabase = _common.get_host_javabase(ctx),
instrument_xslt =
_utils.only(get_android_toolchain(ctx).add_g3itr_xslt.files.to_list()) if ctx.attr.use_xsltproc else None,
xsltproc =
get_android_toolchain(ctx).xsltproc_tool.files_to_run if ctx.attr.use_instrument_xslt else None,
zip_tool = get_android_toolchain(ctx).zip_tool.files_to_run,
)
validation_results = starlark_resources_ctx["validation_results"]
if starlark_resources_ctx["resources_apk"]:
validation_results.append(starlark_resources_ctx["resources_apk"])
return starlark_resources_ctx["providers"] + [
OutputGroupInfo(_validation = validation_results),
_StarlarkResourcesTestingInfo(
r_java = starlark_resources_ctx["r_java"],
),
]
starlark_process = rule(
implementation = _starlark_process_impl,
attrs = dict(
stamp_manifest = attr.bool(
default = True,
),
use_java_package = attr.bool(
default = True,
),
assets = attr.label_list(
allow_files = True,
),
assets_dir = attr.string(),
manifest = attr.label(
allow_single_file = [".xml"],
),
neverlink = attr.bool(),
enable_data_binding = attr.bool(),
custom_package = attr.string(),
resource_files = attr.label_list(
allow_files = True,
),
deps = attr.label_list(
allow_rules = [
"starlark_process",
],
),
exports = attr.label_list(
allow_rules = [
"starlark_process",
],
),
fix_resource_transitivity = attr.bool(default = True),
_host_javabase = attr.label(
cfg = "exec",
default = Label("//tools/jdk:current_java_runtime"),
),
_java_toolchain = attr.label(
default = Label("//tools/jdk:toolchain_android_only"),
),
_manifest_merge_order = attr.label(
default = "//rules/flags:manifest_merge_order",
),
use_xsltproc = attr.bool(default = True),
use_instrument_xslt = attr.bool(default = True),
),
toolchains = [
"//toolchains/android:toolchain_type",
"@bazel_tools//tools/jdk:toolchain_type",
ANDROID_SDK_TOOLCHAIN_TYPE,
],
fragments = [
"android",
"bazel_android", # NOTE: Only exists for Bazel.
],
provides = [_StarlarkResourcesTestingInfo],
_skylark_testable = True,
)
def _starlark_process_test_impl(ctx):
if ctx.attr.expected_starlark_android_resources_info:
if StarlarkAndroidResourcesInfo not in ctx.attr.target_under_test:
fail("StarlarkAndroidResourcesInfo was expected but not provided")
_asserts.provider.starlark_android_resources_info(
ctx.attr.expected_starlark_android_resources_info[StarlarkAndroidResourcesInfo],
ctx.attr.target_under_test[StarlarkAndroidResourcesInfo],
ctx.attr.target_under_test.label,
)
elif StarlarkAndroidResourcesInfo in ctx.attr.target_under_test:
fail("Expected no StarlarkAndroidResourcesInfo, but the provider was found")
_asserts.actions.check_actions(
ctx.attr.inspect_actions,
ctx.attr.target_under_test[Actions],
)
r_java = ctx.attr.target_under_test[_StarlarkResourcesTestingInfo].r_java
runfiles = []
java = None
args = dict(
package = ctx.attr.target_under_test.label.package,
expected_r_class_fields = ",".join(ctx.attr.expected_r_class_fields),
check_r_java = r_java != None,
java = "",
class_path = "",
r_jar_path = "",
)
if r_java:
args["r_jar_path"] = _utils.only(r_java.runtime_output_jars).short_path
args["class_path"] = ":".join(
[args["r_jar_path"], ctx.executable._r_class_check.short_path],
)
runfiles = r_java.runtime_output_jars + [ctx.executable._r_class_check]
java = ctx.attr._host_javabase[java_common.JavaRuntimeInfo]
args["java"] = java.java_executable_exec_path
elif ctx.attr.expected_r_class_fields:
fail("Expected a R.java file but none was generated")
test = ctx.actions.declare_file(ctx.label.name + "/test.sh")
ctx.actions.write(
test,
"""#!/bin/bash
set -eu
EXPECTED_R_CLASS_FIELDS="{expected_r_class_fields}"
if [ "{check_r_java}" == "True" ]; then
# Check the contents of the resources jar, as it is always produced.
# There are cases when it is produced empty (with only META-INF data).
# If there is no R.class generated and the expectation is no resource
# ids, then pass.
set +e # grep may return non zero exit code.
R_JAR_CONTENT="$(unzip -Z1 {r_jar_path} | grep -v 'META-INF')"
set -e
# If the R.jar is empty and expectation is None. Pass.
if [ "${{R_JAR_CONTENT}}" == "" ] && \
[ "${{EXPECTED_R_CLASS_FIELDS}}" == "" ]; then
exit
fi
{java} -cp {class_path} com.google.RClassChecker \
--package="{package}" \
--expected_r_class_fields="${{EXPECTED_R_CLASS_FIELDS}}"
fi
""".format(**args),
is_executable = True,
)
return DefaultInfo(
executable = test,
runfiles = ctx.runfiles(
files = runfiles,
transitive_files = java.files if java else None,
),
)
starlark_process_test = rule(
implementation = _starlark_process_test_impl,
attrs = dict(
_asserts.provider.attrs.items() + _asserts.r_class.attrs.items() + _asserts.actions.attrs.items(),
target_under_test = attr.label(),
_java_toolchain = attr.label(
cfg = "exec",
default = Label("//tools/jdk:toolchain_android_only"),
),
_host_javabase = attr.label(
cfg = "exec",
default = Label("//tools/jdk:current_java_runtime"),
),
),
fragments = ["java"],
test = True,
)
_FakeSplitTransitionTargetInfo = provider(
"Fake Split Transition Target object",
fields = dict(
label = "The target label",
cpu_configuration = "The CPU configuration",
),
)
def _fake_split_transition_target_impl(ctx):
return [
_FakeSplitTransitionTargetInfo(
label = ctx.attr.label,
cpu_configuration = ctx.attr.cpu_configuration,
),
]
_fake_split_transition_target = rule(
implementation = _fake_split_transition_target_impl,
attrs = dict(
label = attr.string(),
cpu_configuration = attr.string(),
),
)
def FakeSplitTransitionTarget(
label,
cpu_configuration,
name = "ignored"): # appease linter
name = label + cpu_configuration
name = ":" + "".join([c for c in name.elems() if c not in [":", "/"]])
_fake_split_transition_target(
name = name[1:],
label = label,
cpu_configuration = cpu_configuration,
)
return name
def _filter_multi_cpu_configuration_targets_test_impl(ctx):
env = unittest.begin(ctx)
expected_filtered_split_targets = [
t[_FakeSplitTransitionTargetInfo]
for t in ctx.attr.expected_filtered_split_targets
]
split_targets = [t[_FakeSplitTransitionTargetInfo] for t in ctx.attr.split_targets]
asserts.equals(
env,
expected_filtered_split_targets,
_resources_testing.filter_multi_cpu_configuration_targets(split_targets),
)
return unittest.end(env)
filter_multi_cpu_configuration_targets_test = unittest.make(
impl = _filter_multi_cpu_configuration_targets_test_impl,
attrs = dict(
split_targets = attr.label_list(),
expected_filtered_split_targets = attr.label_list(),
),
)
def _resources_package_impl(ctx):
java_package = _java.resolve_package_from_label(
ctx.label,
ctx.attr.custom_package,
)
packaged_resources_ctx = _resources.package(
ctx,
assets = ctx.files.assets,
assets_dir = ctx.attr.assets_dir,
resource_files = ctx.files.resource_files,
manifest = ctx.file.manifest,
manifest_values = ctx.attr.manifest_values,
manifest_merge_order = ctx.attr._manifest_merge_order[BuildSettingInfo].value,
instruments = ctx.attr.instruments,
java_package = java_package,
compilation_mode = ctx.attr.compilation_mode,
use_legacy_manifest_merger = ctx.attr.use_legacy_manifest_merger,
deps = ctx.attr.deps,
enable_data_binding = ctx.attr.enable_data_binding,
aapt = get_android_toolchain(ctx).aapt2.files_to_run,
android_jar = get_android_sdk(ctx).android_jar,
legacy_merger = ctx.attr._legacy_merger.files_to_run,
xsltproc = ctx.attr._xsltproc_tool.files_to_run,
instrument_xslt = ctx.file._add_g3itr_xslt,
busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
host_javabase = ctx.attr._host_javabase,
)
return [packaged_resources_ctx] + packaged_resources_ctx.providers
resources_package = rule(
implementation = _resources_package_impl,
attrs = dict(
assets = attr.label_list(
allow_files = True,
),
assets_dir = attr.string(),
manifest = attr.label(
allow_single_file = [".xml"],
),
enable_data_binding = attr.bool(),
instruments = attr.label(),
manifest_values = attr.string_dict(),
custom_package = attr.string(),
resource_files = attr.label_list(
allow_files = True,
),
compilation_mode = attr.string(
default = _compilation_mode.FASTBUILD,
values = [
_compilation_mode.FASTBUILD,
_compilation_mode.DBG,
_compilation_mode.OPT,
],
),
use_legacy_manifest_merger = attr.bool(default = False),
deps = attr.label_list(
providers = [StarlarkAndroidResourcesInfo],
),
_legacy_merger = ANDROID_BINARY_ATTRS.get("_android_manifest_merge_tool"),
_xsltproc_tool = attr.label(
cfg = "exec",
default = Label("//tools/android/xslt:xslt"),
allow_files = True,
),
_add_g3itr_xslt = attr.label(
cfg = "exec",
default = Label("//tools/android/xslt:add_g3itr.xslt"),
allow_single_file = True,
),
_host_javabase = attr.label(
cfg = "exec",
default = Label("//tools/jdk:current_java_runtime"),
),
_manifest_merge_order = attr.label(
default = "//rules/flags:manifest_merge_order",
),
),
toolchains = [
"//toolchains/android:toolchain_type",
"@bazel_tools//tools/jdk:toolchain_type",
ANDROID_SDK_TOOLCHAIN_TYPE,
],
fragments = [
"android",
"bazel_android", # NOTE: Only exists for Bazel.
],
provides = [_resources_testing.ResourcesPackageContextInfo],
)
def _resources_package_test_impl(ctx):
dep = _utils.only(ctx.attr.deps)
packaged_resources_ctx = dep[_resources_testing.ResourcesPackageContextInfo]
manifest = packaged_resources_ctx.processed_manifest
resource_apk = packaged_resources_ctx.resources_apk
class_jar = packaged_resources_ctx.class_jar
aapt = get_android_toolchain(ctx).aapt2.files_to_run.executable
aapt_runfiles = get_android_toolchain(ctx).aapt2[DefaultInfo].default_runfiles.files
test = ctx.actions.declare_file(ctx.label.name + "/test.sh")
ctx.actions.write(
test,
"""#!/bin/bash
set -eu
MANIFEST={manifest}
RESOURCE_APK={resource_apk}
CLASS_JAR={class_jar}
EXPECTED_IS_DEBUGGABLE={expected_is_debuggable}
EXPECTED_ASSETS={expected_assets}
EXPECTED_MANIFEST={expected_manifest}
EXPECTED_JAR_FILES={expected_jar_files}
EXPECTED_RES={expected_res}
AAPT={aapt}
diff_contents () {{
set +e
DIFF=`(diff --ignore-all-space "$1" "$2")`
set -e
if [[ "$DIFF" != "" ]]; then
echo "Error: mismatch between expected and actual: $DIFF"
exit 1
fi
}}
# Only validate expected manifest provided.
if [[ "$EXPECTED_MANIFEST" != "" ]]; then
diff_contents "$EXPECTED_MANIFEST" "$MANIFEST"
fi
JAR_FILES=`(unzip -l "$CLASS_JAR" | sed -e '1,3d' | head -n -2 | tr -s " " | cut -d" " -f5 | sort)`
EXPECTED_JAR_FILES=`(echo "$EXPECTED_JAR_FILES" | tr ',' '\n' | sort)`
diff_contents <( echo "$EXPECTED_JAR_FILES" ) <( echo "$JAR_FILES" )
# Validate debuggable in AndroidManifest.xml
#
# Debuggable is set in the compiled AndroidManifest.xml through an aapt2 flag.
# As such, validating debuggable can only be done by dumping the xmltree.
#
# Sample output, of "aapt2 dump xmltree --file AndroidManifest.xml":
#
# N: android=http://schemas.android.com/apk/res/android (line=1)
# E: manifest (line=1)
# A: http://schemas.android.com/apk/res/android:compileSdkVersion(0x01010572)=30
# A: http://schemas.android.com/apk/res/android:compileSdkVersionCodename(0x01010573)="11" (Raw: "11")
# A: package="com.google.compilation.mode.opt" (Raw: "com.google.compilation.mode.opt")
# A: platformBuildVersionCode=30
# A: platformBuildVersionName=11
# E: uses-sdk (line=2)
# A: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=15
# A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=29
# E: application (line=3)
# A: http://schemas.android.com/apk/res/android:debuggable(0x0101000f)=true'
#
# To mitigate the chances of a false positive, the test retrieves the uri of
# the "android" namespace which is prepended to debuggable and searches for
# the attribute and validates the value.
MANIFEST=$($AAPT dump xmltree $RESOURCE_APK --file AndroidManifest.xml)
ANDROID_NS=$(echo "$MANIFEST" | grep -e "^N: android=" | sed -e "s/^N: android=\\(.\\+\\) .*$/\\1/")
DEBUGGABLE_ATTR="$ANDROID_NS:debuggable"
set +e # Disable failure on non-0 return value, if debuggable entry is missing.
DEBUGGABLE_MANIFEST_ENTRY=$(echo "$MANIFEST" | grep "$DEBUGGABLE_ATTR")
set -e
DEBUGGABLE_VAL=$(echo "$DEBUGGABLE_MANIFEST_ENTRY" | sed -e "s|^.*$DEBUGGABLE_ATTR(.*)=||")
set -x
if [ "$EXPECTED_IS_DEBUGGABLE" == true ] && [[ "$DEBUGGABLE_VAL" == "" ]]; then
echo "Error, expected a debuggable apk, but did not get one."
echo "No debuggable manifest entry found, see manifest:\n$MANIFEST"
exit 1
elif [ "$EXPECTED_IS_DEBUGGABLE" == false ] && [[ "$DEBUGGABLE_VAL" == "true" ]]; then
echo "Error, did not expected a debuggable apk, but get one."
echo "The debuggable manifest entry found: $DEBUGGABLE_MANIFEST_ENTRY"
exit 1
fi
set +x
# Validate resources
RES=$($AAPT dump resources -v $RESOURCE_APK | sed '/^\\s*resource/!d' \
| tr -s ' ' '\t' | cut -f 4 | sort)
EXPECTED_RES=`(echo "$EXPECTED_RES" | tr ',' '\n' | sort)`
diff_contents <( echo "$EXPECTED_RES" ) <( echo "$RES" )
#Validate assets
ASSETS=$(unzip -l $RESOURCE_APK assets/* | tr -s ' ' '\t' | cut -f 5 \
| tail -n +4 | head -n -2)
EXPECTED_ASSETS=`(echo "$EXPECTED_ASSETS" | tr ',' '\n' | sort)`
diff_contents <( echo "$EXPECTED_ASSETS" ) <( echo "$ASSETS" )
""".format(
manifest = manifest.short_path,
resource_apk = resource_apk.short_path,
class_jar = class_jar.short_path,
expected_assets = ",".join(ctx.attr.expected_assets),
expected_jar_files = ",".join([
"'%s'" % filename
for filename in ctx.attr.expected_jar_files
]),
expected_manifest =
ctx.file.expected_manifest.short_path if ctx.attr.expected_manifest else "",
expected_res = ",".join(ctx.attr.expected_res),
expected_is_debuggable = (
"true" if ctx.attr.expected_is_debuggable else "false"
),
aapt = aapt.short_path,
),
is_executable = True,
)
return DefaultInfo(
executable = test,
runfiles = ctx.runfiles(
files = [
manifest,
resource_apk,
class_jar,
aapt,
] + (
[ctx.file.expected_manifest] if ctx.attr.expected_manifest else []
),
transitive_files = aapt_runfiles,
),
)
resources_package_test = rule(
implementation = _resources_package_test_impl,
attrs = dict(
deps = attr.label_list(
providers = [_resources_testing.ResourcesPackageContextInfo],
),
expected_assets = attr.string_list(),
expect_databinding_enabled = attr.bool(default = False),
# APKs are built debuggable, unless built with -c opt. As such the
# default value is True.
expected_is_debuggable = attr.bool(default = True),
expected_jar_files = attr.string_list(),
expected_manifest = attr.label(
allow_single_file = True,
),
expected_res = attr.string_list(),
),
toolchains = ["//toolchains/android:toolchain_type"],
test = True,
)
def _package_resources_final_id_test(ctx):
env = analysistest.begin(ctx)
target_under_test = analysistest.target_under_test(env)
actions = analysistest.target_actions(env)
found_final_r = False
found_nonfinal_r = False
for a in actions:
if a.mnemonic == "StarlarkRClassGenerator":
if "--finalFields" in a.argv:
found_final_r = True
elif "--nofinalFields" in a.argv:
found_nonfinal_r = True
if ctx.attr.final:
# We expect to find both final and non-final if we're building a target
# with final fields because we build both: nonfinal fields for `javac`
# to build against and final fields for the deploy jar.
asserts.true(env, found_final_r, "Missing expected --finalFields")
asserts.true(env, found_nonfinal_r, "Missing expected --nofinalFields")
else:
asserts.false(env, found_final_r, "Unexpected --finalFields")
asserts.true(env, found_nonfinal_r, "Missing expected --nofinalFields")
return analysistest.end(env)
package_resources_final_id_test = analysistest.make(
_package_resources_final_id_test,
attrs = {
"final": attr.bool(mandatory = True),
},
)