From 7289309a538e838fced9ad86f5a9f64b51bf8426 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Thu, 12 Feb 2026 11:11:57 +0100 Subject: [PATCH] Extract embedded proguard specs from JARs and AARs during R8 --- rules/aar_import/impl.bzl | 7 +- rules/android_binary/r8.bzl | 19 +++ rules/flags/flag_defs.bzl | 14 ++ toolchains/android/toolchain.bzl | 6 + tools/android/BUILD | 33 ++++- .../aar_embedded_proguard_extractor.py | 22 +-- .../aar_embedded_proguard_extractor_test.py | 130 ++++++++++++++++-- .../jar_embedded_proguard_extractor.py | 68 +++++++++ .../jar_embedded_proguard_extractor_test.py | 108 +++++++++++++++ tools/android/proguard_extractor_lib.py | 81 +++++++++++ 10 files changed, 468 insertions(+), 20 deletions(-) create mode 100644 tools/android/jar_embedded_proguard_extractor.py create mode 100644 tools/android/jar_embedded_proguard_extractor_test.py create mode 100644 tools/android/proguard_extractor_lib.py diff --git a/rules/aar_import/impl.bzl b/rules/aar_import/impl.bzl index 527c5a587..2aab29b4d 100644 --- a/rules/aar_import/impl.bzl +++ b/rules/aar_import/impl.bzl @@ -39,6 +39,7 @@ load( _utils = "utils", ) load("//rules:visibility.bzl", "PROJECT_VISIBILITY") +load("//rules/flags:flags.bzl", _flags = "flags") load("@rules_java//java/common:java_common.bzl", "java_common") load("@rules_java//java/common:java_info.bzl", "JavaInfo") load("@rules_java//java/common:proguard_spec_info.bzl", "ProguardSpecInfo") @@ -432,10 +433,13 @@ def _collect_proguard( ctx, out_proguard, aar, - aar_embedded_proguard_extractor): + aar_embedded_proguard_extractor, + extract_r8_rules = False): args = ctx.actions.args() args.add("--input_aar", aar) args.add("--output_proguard_file", out_proguard) + if extract_r8_rules: + args.add("--extract_r8_rules") ctx.actions.run( executable = aar_embedded_proguard_extractor, arguments = [args], @@ -566,6 +570,7 @@ def impl(ctx): proguard_spec, aar, _get_android_toolchain(ctx).aar_embedded_proguard_extractor.files_to_run, + extract_r8_rules = _flags.get(ctx).aar_import_extract_r8_rules, )) lint_providers = _process_lint_rules( diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 624295d99..3627cefc9 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -33,6 +33,7 @@ load( "utils", ) load("//rules:visibility.bzl", "PROJECT_VISIBILITY") +load("//rules/flags:flags.bzl", _flags = "flags") visibility(PROJECT_VISIBILITY) @@ -82,6 +83,24 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ android_jar = get_android_sdk(ctx).android_jar proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config) + # Optionally extract proguard specs embedded in the deploy JAR (META-INF/proguard/ + # and META-INF/com.android.tools/) so they are passed to R8. + if _flags.get(ctx).r8_extract_embedded_proguard_specs: + 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_jar", deploy_jar) + jar_extractor_args.add("--output_proguard_file", jar_embedded_proguard) + ctx.actions.run( + executable = get_android_toolchain(ctx).jar_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, + ) + proguard_specs = proguard_specs + [jar_embedded_proguard] + # Get min SDK version from attribute, manifest_values, or depot floor effective_min_sdk = min_sdk_version.DEPOT_FLOOR min_sdk_attr = getattr(ctx.attr, "min_sdk_version", 0) diff --git a/rules/flags/flag_defs.bzl b/rules/flags/flag_defs.bzl index 8ac19c119..5dde68d51 100644 --- a/rules/flags/flag_defs.bzl +++ b/rules/flags/flag_defs.bzl @@ -94,3 +94,17 @@ def define_flags(): default = False, description = "For testing/validation only. Use baseline profiles as startup profiles in optimized builds.", ) + + flags.DEFINE_bool( + name = "r8_extract_embedded_proguard_specs", + default = False, + description = "When enabled, R8 extracts embedded proguard specs from META-INF/proguard/ " + + "and META-INF/com.android.tools/ in the deploy JAR and passes them to R8.", + ) + + flags.DEFINE_bool( + name = "aar_import_extract_r8_rules", + default = False, + description = "When enabled, aar_import extracts R8-targeted proguard rules from " + + "META-INF/com.android.tools/ inside classes.jar in addition to proguard.txt.", + ) diff --git a/toolchains/android/toolchain.bzl b/toolchains/android/toolchain.bzl index 98dd0f32a..98fb7773e 100644 --- a/toolchains/android/toolchain.bzl +++ b/toolchains/android/toolchain.bzl @@ -37,6 +37,12 @@ _ATTRS = dict( default = "//tools/android:aar_embedded_proguard_extractor", executable = True, ), + jar_embedded_proguard_extractor = attr.label( + allow_files = True, + cfg = "exec", + default = "//tools/android:jar_embedded_proguard_extractor", + executable = True, + ), aar_native_libs_zip_creator = attr.label( allow_files = True, cfg = "exec", diff --git a/tools/android/BUILD b/tools/android/BUILD index a67aa0f35..f3ed0ffe1 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -426,6 +426,12 @@ py_binary( ], ) +py_library( + name = "proguard_extractor_lib", + srcs = ["proguard_extractor_lib.py"], + visibility = ["//visibility:private"], +) + py_binary( name = "aar_embedded_proguard_extractor", srcs = ["aar_embedded_proguard_extractor.py"], @@ -433,6 +439,19 @@ py_binary( deps = [ ":json_worker_wrapper", ":junction_lib", + ":proguard_extractor_lib", + "@py_absl//absl:app", + ], +) + +py_binary( + name = "jar_embedded_proguard_extractor", + srcs = ["jar_embedded_proguard_extractor.py"], + visibility = ["//visibility:public"], + deps = [ + ":json_worker_wrapper", + ":junction_lib", + ":proguard_extractor_lib", "@py_absl//absl:app", ], ) @@ -476,7 +495,19 @@ py_test( py_test( name = "aar_embedded_proguard_extractor_test", srcs = ["aar_embedded_proguard_extractor_test.py"], - deps = [":aar_embedded_proguard_extractor"], + deps = [ + ":aar_embedded_proguard_extractor", + ":proguard_extractor_lib", + ], +) + +py_test( + name = "jar_embedded_proguard_extractor_test", + srcs = ["jar_embedded_proguard_extractor_test.py"], + deps = [ + ":jar_embedded_proguard_extractor", + ":proguard_extractor_lib", + ], ) py_test( diff --git a/tools/android/aar_embedded_proguard_extractor.py b/tools/android/aar_embedded_proguard_extractor.py index c8f5c2c8e..24da90cc5 100644 --- a/tools/android/aar_embedded_proguard_extractor.py +++ b/tools/android/aar_embedded_proguard_extractor.py @@ -27,6 +27,7 @@ from tools.android import json_worker_wrapper from tools.android import junction +from tools.android import proguard_extractor_lib FLAGS = flags.FLAGS @@ -35,21 +36,23 @@ flags.DEFINE_string("output_proguard_file", None, "Output parameter file for proguard") flags.mark_flag_as_required("output_proguard_file") +flags.DEFINE_boolean("extract_r8_rules", False, + "Also extract R8-targeted rules from classes.jar") # 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 ExtractEmbeddedProguard(aar, output, extract_r8_rules=False): + if extract_r8_rules: + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, output) + else: + proguard_extractor_lib.ExtractEmbeddedProguardFromAarLegacy(aar, output) -def _Main(input_aar, output_proguard_file): +def _Main(input_aar, output_proguard_file, extract_r8_rules): with zipfile.ZipFile(input_aar, "r") as aar: with open(output_proguard_file, "wb") as output: - ExtractEmbeddedProguard(aar, output) + ExtractEmbeddedProguard(aar, output, extract_r8_rules) def main(unused_argv): @@ -65,9 +68,10 @@ def main(unused_argv): 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))) + os.path.join(proguard_junc, os.path.basename(proguard_long)), + FLAGS.extract_r8_rules) else: - _Main(FLAGS.input_aar, FLAGS.output_proguard_file) + _Main(FLAGS.input_aar, FLAGS.output_proguard_file, FLAGS.extract_r8_rules) if __name__ == "__main__": diff --git a/tools/android/aar_embedded_proguard_extractor_test.py b/tools/android/aar_embedded_proguard_extractor_test.py index 457520f67..4ff24afde 100644 --- a/tools/android/aar_embedded_proguard_extractor_test.py +++ b/tools/android/aar_embedded_proguard_extractor_test.py @@ -19,19 +19,14 @@ import zipfile from tools.android import aar_embedded_proguard_extractor +from tools.android import proguard_extractor_lib -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) +class AarEmbeddedProguardExtractorLegacyTest(unittest.TestCase): + """Unit tests for AAR proguard extraction (legacy behavior, extract_r8_rules=False).""" def setUp(self): - super(AarEmbeddedProguardExtractor, self).setUp() + super(AarEmbeddedProguardExtractorLegacyTest, self).setUp() os.chdir(os.environ["TEST_TMPDIR"]) def testNoProguardTxt(self): @@ -49,6 +44,123 @@ def testWithProguardTxt(self): proguard_file.seek(0) self.assertEqual(b"hello world", proguard_file.read()) + def _makeClassesJar(self, 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 testR8RulesFromClassesJarIgnoredByDefault(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() + aar_embedded_proguard_extractor.ExtractEmbeddedProguard(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + +class AarEmbeddedProguardExtractorWithR8RulesTest(unittest.TestCase): + """Unit tests for AAR proguard extraction with extract_r8_rules=True.""" + + def setUp(self): + super(AarEmbeddedProguardExtractorWithR8RulesTest, self).setUp() + os.chdir(os.environ["TEST_TMPDIR"]) + + def _makeClassesJar(self, 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 testNoProguardTxt(self): + aar = zipfile.ZipFile(io.BytesIO(), "w") + proguard_file = io.BytesIO() + proguard_extractor_lib.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() + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"hello world", proguard_file.read()) + + 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() + proguard_extractor_lib.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() + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(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() + proguard_extractor_lib.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() + proguard_extractor_lib.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() + proguard_extractor_lib.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() + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + if __name__ == "__main__": unittest.main() diff --git a/tools/android/jar_embedded_proguard_extractor.py b/tools/android/jar_embedded_proguard_extractor.py new file mode 100644 index 000000000..7b24383cc --- /dev/null +++ b/tools/android/jar_embedded_proguard_extractor.py @@ -0,0 +1,68 @@ +# 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.""" + +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 +from tools.android import proguard_extractor_lib + +FLAGS = flags.FLAGS + +flags.DEFINE_string("input_jar", None, "Input JAR") +flags.mark_flag_as_required("input_jar") +flags.DEFINE_string("output_proguard_file", None, + "Output parameter file for proguard") +flags.mark_flag_as_required("output_proguard_file") + + +def ExtractEmbeddedProguard(jar, output): + """Extract proguard specs from a JAR file.""" + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, output) + + +def _Main(input_jar, output_proguard_file): + with zipfile.ZipFile(input_jar, "r") as jar: + with open(output_proguard_file, "wb") as output: + ExtractEmbeddedProguard(jar, output) + + +def main(unused_argv): + if os.name == "nt": + jar_long = os.path.abspath(FLAGS.input_jar) + proguard_long = os.path.abspath(FLAGS.output_proguard_file) + + with junction.TempJunction(os.path.dirname(jar_long)) as jar_junc: + with junction.TempJunction( + os.path.dirname(proguard_long)) as proguard_junc: + _Main( + os.path.join(jar_junc, os.path.basename(jar_long)), + os.path.join(proguard_junc, os.path.basename(proguard_long))) + else: + _Main(FLAGS.input_jar, FLAGS.output_proguard_file) + + +if __name__ == "__main__": + json_worker_wrapper.wrap_worker(FLAGS, main, app.run) diff --git a/tools/android/jar_embedded_proguard_extractor_test.py b/tools/android/jar_embedded_proguard_extractor_test.py new file mode 100644 index 000000000..4704dfb7c --- /dev/null +++ b/tools/android/jar_embedded_proguard_extractor_test.py @@ -0,0 +1,108 @@ +# 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 jar_embedded_proguard_extractor.""" + +import io +import os +import unittest +import zipfile + +from tools.android import proguard_extractor_lib + + +class JarEmbeddedProguardExtractorTest(unittest.TestCase): + """Unit tests for JAR proguard extraction.""" + + def setUp(self): + super(JarEmbeddedProguardExtractorTest, self).setUp() + os.chdir(os.environ["TEST_TMPDIR"]) + + def testNoProguardSpecs(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + proguard_file = io.BytesIO() + proguard_extractor_lib.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() + proguard_extractor_lib.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() + proguard_extractor_lib.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() + proguard_extractor_lib.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() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(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() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + 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() + proguard_extractor_lib.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() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_file.seek(0) + self.assertEqual(b"", proguard_file.read()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py new file mode 100644 index 000000000..bdbb2eede --- /dev/null +++ b/tools/android/proguard_extractor_lib.py @@ -0,0 +1,81 @@ +# 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. +"""Shared library for extracting proguard spec files from JARs and AARs.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import io +import zipfile + + +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. + + Reads both legacy META-INF/proguard/ and R8-targeted + META-INF/com.android.tools/ entries. + """ + 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. + + Reads proguard.txt from the AAR root, and also extracts R8 rules + from META-INF/com.android.tools/ inside classes.jar. + """ + 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 ExtractEmbeddedProguardFromAarLegacy(aar, output): + """Extract proguard specs from an AAR file (legacy behavior). + + Only reads proguard.txt from the AAR root. Does not extract R8 rules + from classes.jar. + """ + proguard_spec = "proguard.txt" + + if proguard_spec in aar.namelist(): + output.write(aar.read(proguard_spec))