diff --git a/rules/aar_import/impl.bzl b/rules/aar_import/impl.bzl index 527c5a587..e7bc5f60f 100644 --- a/rules/aar_import/impl.bzl +++ b/rules/aar_import/impl.bzl @@ -434,8 +434,9 @@ def _collect_proguard( aar, aar_embedded_proguard_extractor): args = ctx.actions.args() - args.add("--input_aar", aar) + args.add("--input_archive", aar) args.add("--output_proguard_file", out_proguard) + args.add("--archive_type", "aar") ctx.actions.run( executable = aar_embedded_proguard_extractor, arguments = [args], @@ -565,7 +566,7 @@ def impl(ctx): ctx, proguard_spec, aar, - _get_android_toolchain(ctx).aar_embedded_proguard_extractor.files_to_run, + _get_android_toolchain(ctx).archive_embedded_proguard_extractor.files_to_run, )) lint_providers = _process_lint_rules( diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 624295d99..452be4908 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -79,8 +79,25 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ dexes_zip = ctx.actions.declare_file(ctx.label.name + "_dexes.zip") proguard_mappings_output_file = ctx.actions.declare_file(ctx.label.name + "_proguard.map") + # Extract proguard specs embedded in the deploy JAR (META-INF/proguard/ + # and META-INF/com.android.tools/) so they are passed to R8. + jar_embedded_proguard = ctx.actions.declare_file(ctx.label.name + "_jar_embedded_proguard.pro") + jar_extractor_args = ctx.actions.args() + jar_extractor_args.add("--input_archive", deploy_jar) + jar_extractor_args.add("--output_proguard_file", jar_embedded_proguard) + jar_extractor_args.add("--archive_type", "jar") + ctx.actions.run( + executable = get_android_toolchain(ctx).archive_embedded_proguard_extractor.files_to_run, + arguments = [jar_extractor_args], + inputs = [deploy_jar], + outputs = [jar_embedded_proguard], + mnemonic = "JarEmbeddedProguardExtractor", + progress_message = "Extracting proguard specs from deploy jar for %{label}", + toolchain = None, + ) + android_jar = get_android_sdk(ctx).android_jar - proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config) + proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config) + [jar_embedded_proguard] # Get min SDK version from attribute, manifest_values, or depot floor effective_min_sdk = min_sdk_version.DEPOT_FLOOR diff --git a/toolchains/android/toolchain.bzl b/toolchains/android/toolchain.bzl index 98dd0f32a..9e918f5cd 100644 --- a/toolchains/android/toolchain.bzl +++ b/toolchains/android/toolchain.bzl @@ -31,10 +31,10 @@ _ATTRS = dict( default = "//tools/android:aar_embedded_jars_extractor", executable = True, ), - aar_embedded_proguard_extractor = attr.label( + archive_embedded_proguard_extractor = attr.label( allow_files = True, cfg = "exec", - default = "//tools/android:aar_embedded_proguard_extractor", + default = "//tools/android:archive_embedded_proguard_extractor", executable = True, ), aar_native_libs_zip_creator = attr.label( diff --git a/tools/android/BUILD b/tools/android/BUILD index a67aa0f35..18ea237dc 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -427,8 +427,8 @@ py_binary( ) py_binary( - name = "aar_embedded_proguard_extractor", - srcs = ["aar_embedded_proguard_extractor.py"], + name = "archive_embedded_proguard_extractor", + srcs = ["archive_embedded_proguard_extractor.py"], visibility = ["//visibility:public"], deps = [ ":json_worker_wrapper", @@ -474,9 +474,9 @@ py_test( ) py_test( - name = "aar_embedded_proguard_extractor_test", - srcs = ["aar_embedded_proguard_extractor_test.py"], - deps = [":aar_embedded_proguard_extractor"], + name = "archive_embedded_proguard_extractor_test", + srcs = ["archive_embedded_proguard_extractor_test.py"], + deps = [":archive_embedded_proguard_extractor"], ) py_test( diff --git a/tools/android/aar_embedded_proguard_extractor.py b/tools/android/aar_embedded_proguard_extractor.py deleted file mode 100644 index c8f5c2c8e..000000000 --- a/tools/android/aar_embedded_proguard_extractor.py +++ /dev/null @@ -1,74 +0,0 @@ -# pylint: disable=g-direct-third-party-import -# Copyright 2021 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. -"""A tool for extracting the proguard spec file from an AAR.""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import zipfile - -# Do not edit this line. Copybara replaces it with PY2 migration helper. -from absl import app -from absl import flags - -from tools.android import json_worker_wrapper -from tools.android import junction - -FLAGS = flags.FLAGS - -flags.DEFINE_string("input_aar", None, "Input AAR") -flags.mark_flag_as_required("input_aar") -flags.DEFINE_string("output_proguard_file", None, - "Output parameter file for proguard") -flags.mark_flag_as_required("output_proguard_file") - - -# Attempt to extract proguard spec from AAR. If the file doesn't exist, an empty -# proguard spec file will be created -def ExtractEmbeddedProguard(aar, output): - proguard_spec = "proguard.txt" - - if proguard_spec in aar.namelist(): - output.write(aar.read(proguard_spec)) - - -def _Main(input_aar, output_proguard_file): - with zipfile.ZipFile(input_aar, "r") as aar: - with open(output_proguard_file, "wb") as output: - ExtractEmbeddedProguard(aar, output) - - -def main(unused_argv): - if os.name == "nt": - # Shorten paths unconditionally, because the extracted paths in - # ExtractEmbeddedJars (which we cannot yet predict, because they depend on - # the names of the Zip entries) may be longer than MAX_PATH. - aar_long = os.path.abspath(FLAGS.input_aar) - proguard_long = os.path.abspath(FLAGS.output_proguard_file) - - with junction.TempJunction(os.path.dirname(aar_long)) as aar_junc: - with junction.TempJunction( - os.path.dirname(proguard_long)) as proguard_junc: - _Main( - os.path.join(aar_junc, os.path.basename(aar_long)), - os.path.join(proguard_junc, os.path.basename(proguard_long))) - else: - _Main(FLAGS.input_aar, FLAGS.output_proguard_file) - - -if __name__ == "__main__": - json_worker_wrapper.wrap_worker(FLAGS, main, app.run) diff --git a/tools/android/aar_embedded_proguard_extractor_test.py b/tools/android/aar_embedded_proguard_extractor_test.py deleted file mode 100644 index 457520f67..000000000 --- a/tools/android/aar_embedded_proguard_extractor_test.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2021 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. -"""Tests for aar_embedded_proguard_extractor.""" - -import io -import os -import unittest -import zipfile - -from tools.android import aar_embedded_proguard_extractor - - -class AarEmbeddedProguardExtractor(unittest.TestCase): - """Unit tests for aar_embedded_proguard_extractor.py.""" - - # Python 2 alias - if not hasattr(unittest.TestCase, "assertCountEqual"): - - def assertCountEqual(self, *args): - return self.assertItemsEqual(*args) - - def setUp(self): - super(AarEmbeddedProguardExtractor, self).setUp() - os.chdir(os.environ["TEST_TMPDIR"]) - - def testNoProguardTxt(self): - aar = zipfile.ZipFile(io.BytesIO(), "w") - proguard_file = io.BytesIO() - aar_embedded_proguard_extractor.ExtractEmbeddedProguard(aar, proguard_file) - proguard_file.seek(0) - self.assertEqual(b"", proguard_file.read()) - - def testWithProguardTxt(self): - aar = zipfile.ZipFile(io.BytesIO(), "w") - aar.writestr("proguard.txt", "hello world") - proguard_file = io.BytesIO() - aar_embedded_proguard_extractor.ExtractEmbeddedProguard(aar, proguard_file) - proguard_file.seek(0) - self.assertEqual(b"hello world", proguard_file.read()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/android/archive_embedded_proguard_extractor.py b/tools/android/archive_embedded_proguard_extractor.py new file mode 100644 index 000000000..50ab78caa --- /dev/null +++ b/tools/android/archive_embedded_proguard_extractor.py @@ -0,0 +1,109 @@ +# pylint: disable=g-direct-third-party-import +# Copyright 2021 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. +"""A tool for extracting proguard spec files from a JAR or AAR.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import io +import os +import zipfile + +# Do not edit this line. Copybara replaces it with PY2 migration helper. +from absl import app +from absl import flags + +from tools.android import json_worker_wrapper +from tools.android import junction + +FLAGS = flags.FLAGS + +flags.DEFINE_string("input_archive", None, "Input JAR or AAR") +flags.mark_flag_as_required("input_archive") +flags.DEFINE_string("output_proguard_file", None, + "Output parameter file for proguard") +flags.mark_flag_as_required("output_proguard_file") +flags.DEFINE_enum("archive_type", None, ["jar", "aar"], + "Type of archive: jar or aar") +flags.mark_flag_as_required("archive_type") + + +def _ExtractR8Rules(jar, output): + """Extract R8 rules from META-INF/com.android.tools/ inside a JAR. + + Handles subdirectories like r8-from-X-upto-Y/. All matching files are + concatenated into the output, sorted by path for determinism. + """ + meta_inf_prefix = "META-INF/com.android.tools/" + for entry in sorted(jar.namelist()): + if entry.startswith(meta_inf_prefix) and not entry.endswith("/"): + output.write(b"\n") + output.write(jar.read(entry)) + + +def ExtractEmbeddedProguardFromJar(jar, output): + """Extract proguard specs from a JAR file.""" + legacy_prefix = "META-INF/proguard/" + r8_prefix = "META-INF/com.android.tools/" + + for entry in sorted(jar.namelist()): + if not entry.endswith("/") and ( + entry.startswith(legacy_prefix) or entry.startswith(r8_prefix)): + output.write(b"\n") + output.write(jar.read(entry)) + + +def ExtractEmbeddedProguardFromAar(aar, output): + """Extract proguard specs from an AAR file.""" + proguard_spec = "proguard.txt" + classes_jar = "classes.jar" + + if proguard_spec in aar.namelist(): + output.write(aar.read(proguard_spec)) + + # For AARs, META-INF/com.android.tools/ lives inside classes.jar + if classes_jar in aar.namelist(): + with zipfile.ZipFile(io.BytesIO(aar.read(classes_jar)), "r") as jar: + _ExtractR8Rules(jar, output) + + +def _Main(input_archive, output_proguard_file, archive_type): + with zipfile.ZipFile(input_archive, "r") as archive: + with open(output_proguard_file, "wb") as output: + if archive_type == "jar": + ExtractEmbeddedProguardFromJar(archive, output) + else: + ExtractEmbeddedProguardFromAar(archive, output) + + +def main(unused_argv): + if os.name == "nt": + archive_long = os.path.abspath(FLAGS.input_archive) + proguard_long = os.path.abspath(FLAGS.output_proguard_file) + + with junction.TempJunction(os.path.dirname(archive_long)) as archive_junc: + with junction.TempJunction( + os.path.dirname(proguard_long)) as proguard_junc: + _Main( + os.path.join(archive_junc, os.path.basename(archive_long)), + os.path.join(proguard_junc, os.path.basename(proguard_long)), + FLAGS.archive_type) + else: + _Main(FLAGS.input_archive, FLAGS.output_proguard_file, FLAGS.archive_type) + + +if __name__ == "__main__": + json_worker_wrapper.wrap_worker(FLAGS, main, app.run) diff --git a/tools/android/archive_embedded_proguard_extractor_test.py b/tools/android/archive_embedded_proguard_extractor_test.py new file mode 100644 index 000000000..5ec3e785d --- /dev/null +++ b/tools/android/archive_embedded_proguard_extractor_test.py @@ -0,0 +1,217 @@ +# Copyright 2021 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. +"""Tests for archive_embedded_proguard_extractor.""" + +import io +import os +import unittest +import zipfile + +from tools.android import archive_embedded_proguard_extractor + + +class JarEmbeddedProguardExtractor(unittest.TestCase): + """Unit tests for JAR extraction in archive_embedded_proguard_extractor.py.""" + + def setUp(self): + super(JarEmbeddedProguardExtractor, self).setUp() + os.chdir(os.environ["TEST_TMPDIR"]) + + def testNoProguardSpecs(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + def testLegacyMetaInfProguard(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/proguard/rules.pro", "-keep class A") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class A", proguard_file.read()) + + def testMultipleLegacyFiles(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/proguard/rules1.pro", "-keep class A") + jar.writestr("META-INF/proguard/rules2.pro", "-keep class B") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class A\n-keep class B", proguard_file.read()) + + def testR8Rules(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/com.android.tools/r8/rules.pro", "-keep class C") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class C", proguard_file.read()) + + def testR8RulesVersionedSubdirs(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr( + "META-INF/com.android.tools/r8-from-8.0.0/rules.pro", "-keep class D") + jar.writestr( + "META-INF/com.android.tools/r8-upto-8.0.0/rules.pro", "-keep class E") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + # Sorted by path: r8-from-8.0.0 before r8-upto-8.0.0 + self.assertEqual( + b"\n-keep class D\n-keep class E", proguard_file.read()) + + def testLegacyAndR8RulesCombined(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/proguard/rules.pro", "-keep class F") + jar.writestr("META-INF/com.android.tools/r8/rules.pro", "-keep class G") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + # Sorted by path: META-INF/com.android.tools before META-INF/proguard + self.assertEqual( + b"\n-keep class G\n-keep class F", proguard_file.read()) + + def testIgnoresDirectoryEntries(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/proguard/", "") + jar.writestr("META-INF/com.android.tools/", "") + jar.writestr("META-INF/com.android.tools/r8/", "") + jar.writestr("META-INF/com.android.tools/r8/rules.pro", "-keep class H") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class H", proguard_file.read()) + + def testIgnoresUnrelatedMetaInf(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr("META-INF/MANIFEST.MF", "Manifest-Version: 1.0") + jar.writestr("META-INF/services/com.example.Spi", "com.example.SpiImpl") + jar.writestr("com/example/Foo.class", "classdata") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + +class AarEmbeddedProguardExtractor(unittest.TestCase): + """Unit tests for AAR extraction in archive_embedded_proguard_extractor.py.""" + + # Python 2 alias + if not hasattr(unittest.TestCase, "assertCountEqual"): + + def assertCountEqual(self, *args): + return self.assertItemsEqual(*args) + + def setUp(self): + super(AarEmbeddedProguardExtractor, self).setUp() + os.chdir(os.environ["TEST_TMPDIR"]) + + def testNoProguardTxt(self): + aar = zipfile.ZipFile(io.BytesIO(), "w") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + def testWithProguardTxt(self): + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("proguard.txt", "hello world") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"hello world", proguard_file.read()) + + def _makeClassesJar(self, entries): + """Create an in-memory classes.jar with the given {path: content} entries.""" + jar_buf = io.BytesIO() + with zipfile.ZipFile(jar_buf, "w") as jar: + for path, content in entries.items(): + jar.writestr(path, content) + return jar_buf.getvalue() + + def testR8RulesFromClassesJar(self): + classes_jar = self._makeClassesJar({ + "META-INF/com.android.tools/r8/rules.pro": "-keep class A", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class A", proguard_file.read()) + + def testR8RulesFromVersionedSubdirs(self): + classes_jar = self._makeClassesJar({ + "META-INF/com.android.tools/r8-from-8.0.0/rules.pro": "-keep class B", + "META-INF/com.android.tools/r8-upto-8.0.0/rules.pro": "-keep class C", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + # Sorted by path: r8-from-8.0.0 before r8-upto-8.0.0 + self.assertEqual( + b"\n-keep class B\n-keep class C", proguard_file.read()) + + def testR8RulesAndProguardTxtCombined(self): + classes_jar = self._makeClassesJar({ + "META-INF/com.android.tools/r8/rules.pro": "-keep class D", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("proguard.txt", "-keep class E") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual( + b"-keep class E\n-keep class D", proguard_file.read()) + + def testR8RulesIgnoresDirectoryEntries(self): + classes_jar = self._makeClassesJar({ + "META-INF/com.android.tools/": "", + "META-INF/com.android.tools/r8/": "", + "META-INF/com.android.tools/r8/rules.pro": "-keep class F", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class F", proguard_file.read()) + + def testNoClassesJarNoR8Rules(self): + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("some_other_file.txt", "data") + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + def testClassesJarWithoutR8Rules(self): + classes_jar = self._makeClassesJar({ + "com/example/Foo.class": "classdata", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + archive_embedded_proguard_extractor.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + +if __name__ == "__main__": + unittest.main()