From 32e17ef4cf76ef1315a0e78f24869b185d3e2b24 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 10:33:34 +0200 Subject: [PATCH 1/9] Read R8 version from runfiles in proguard extractors Add runfiles dependency and data attribute to both AAR and JAR proguard extractor targets, and read the R8 version from r8.version at runtime. --- tools/android/BUILD | 13 +++++++++++++ tools/android/aar_embedded_proguard_extractor.py | 9 +++++++++ tools/android/jar_embedded_proguard_extractor.py | 9 +++++++++ 3 files changed, 31 insertions(+) diff --git a/tools/android/BUILD b/tools/android/BUILD index b1b76ecf1..79edef5d9 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -435,11 +435,13 @@ py_library( py_binary( name = "aar_embedded_proguard_extractor", srcs = ["aar_embedded_proguard_extractor.py"], + data = [":r8_version"], visibility = ["//visibility:public"], deps = [ ":json_worker_wrapper", ":junction_lib", ":proguard_extractor_lib", + "@bazel_tools//tools/python/runfiles", "@py_absl//absl:app", ], ) @@ -447,11 +449,13 @@ py_binary( py_binary( name = "jar_embedded_proguard_extractor", srcs = ["jar_embedded_proguard_extractor.py"], + data = [":r8_version"], visibility = ["//visibility:public"], deps = [ ":json_worker_wrapper", ":junction_lib", ":proguard_extractor_lib", + "@bazel_tools//tools/python/runfiles", "@py_absl//absl:app", ], ) @@ -660,3 +664,12 @@ genrule( """, visibility = ["//visibility:public"], ) + +genrule( + name = "r8_version", + outs = [ + "r8.version", + ], + cmd = "$(location :r8) --version | awk -F' ' '{ print $$2 }' >$(OUTS)", + tools = [":r8"], +) diff --git a/tools/android/aar_embedded_proguard_extractor.py b/tools/android/aar_embedded_proguard_extractor.py index e9149f735..825d47a8d 100644 --- a/tools/android/aar_embedded_proguard_extractor.py +++ b/tools/android/aar_embedded_proguard_extractor.py @@ -18,6 +18,8 @@ from __future__ import division from __future__ import print_function +from bazel_tools.tools.python.runfiles import runfiles + import os import zipfile @@ -57,6 +59,13 @@ def _Main(input_aar, output_proguard_file, extract_r8_rules): def main(unused_argv): + r = runfiles.Create() + r8_version = None + with open(r.Rlocation("rules_android/tools/android/r8.version"), "r") as file: + runfile_lines = file.readlines() + if runfile_lines: + r8_version = runfile_lines[0].strip() + if os.name == "nt": # Shorten paths unconditionally, because the extracted paths in # ExtractEmbeddedJars (which we cannot yet predict, because they depend on diff --git a/tools/android/jar_embedded_proguard_extractor.py b/tools/android/jar_embedded_proguard_extractor.py index bdd93a116..1ed05bf1a 100644 --- a/tools/android/jar_embedded_proguard_extractor.py +++ b/tools/android/jar_embedded_proguard_extractor.py @@ -18,6 +18,8 @@ from __future__ import division from __future__ import print_function +from bazel_tools.tools.python.runfiles import runfiles + import os import zipfile @@ -51,6 +53,13 @@ def _Main(input_jar, output_proguard_file): def main(unused_argv): + r = runfiles.Create() + r8_version = None + with open(r.Rlocation("rules_android/tools/android/r8.version"), "r") as file: + runfile_lines = file.readlines() + if runfile_lines: + r8_version = runfile_lines[0].strip() + if os.name == "nt": jar_long = os.path.abspath(FLAGS.input_jar) proguard_long = os.path.abspath(FLAGS.output_proguard_file) From 3958338c0596a8455d25ac38e5e59131165b1f18 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 10:34:45 +0200 Subject: [PATCH 2/9] Pass r8_version through extractor function signatures Thread r8_version from main() through _Main and into all proguard_extractor_lib functions. No behavioral change yet. --- .../android/aar_embedded_proguard_extractor.py | 17 ++++------------- .../android/jar_embedded_proguard_extractor.py | 11 ++++++----- tools/android/proguard_extractor_lib.py | 10 +++++----- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/tools/android/aar_embedded_proguard_extractor.py b/tools/android/aar_embedded_proguard_extractor.py index 825d47a8d..013d01f12 100644 --- a/tools/android/aar_embedded_proguard_extractor.py +++ b/tools/android/aar_embedded_proguard_extractor.py @@ -43,19 +43,10 @@ ) -# 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, 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, extract_r8_rules): +def _Main(input_aar, output_proguard_file, r8_version = None): with zipfile.ZipFile(input_aar, "r") as aar: with open(output_proguard_file, "wb") as output: - ExtractEmbeddedProguard(aar, output, extract_r8_rules) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, output, r8_version) def main(unused_argv): @@ -79,10 +70,10 @@ def main(unused_argv): _Main( os.path.join(aar_junc, os.path.basename(aar_long)), os.path.join(proguard_junc, os.path.basename(proguard_long)), - FLAGS.extract_r8_rules, + r8_version ) else: - _Main(FLAGS.input_aar, FLAGS.output_proguard_file, FLAGS.extract_r8_rules) + _Main(FLAGS.input_aar, FLAGS.output_proguard_file, r8_version) if __name__ == "__main__": diff --git a/tools/android/jar_embedded_proguard_extractor.py b/tools/android/jar_embedded_proguard_extractor.py index 1ed05bf1a..b10a7fdf7 100644 --- a/tools/android/jar_embedded_proguard_extractor.py +++ b/tools/android/jar_embedded_proguard_extractor.py @@ -41,15 +41,15 @@ flags.mark_flag_as_required("output_proguard_file") -def ExtractEmbeddedProguard(jar, output): +def ExtractEmbeddedProguard(jar, output, r8_version): """Extract proguard specs from a JAR file.""" - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, output) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, output, r8_version) -def _Main(input_jar, output_proguard_file): +def _Main(input_jar, output_proguard_file, r8_version = None): with zipfile.ZipFile(input_jar, "r") as jar: with open(output_proguard_file, "wb") as output: - ExtractEmbeddedProguard(jar, output) + ExtractEmbeddedProguard(jar, output, r8_version) def main(unused_argv): @@ -71,9 +71,10 @@ def main(unused_argv): _Main( os.path.join(jar_junc, os.path.basename(jar_long)), os.path.join(proguard_junc, os.path.basename(proguard_long)), + r8_version ) else: - _Main(FLAGS.input_jar, FLAGS.output_proguard_file) + _Main(FLAGS.input_jar, FLAGS.output_proguard_file, r8_version) if __name__ == "__main__": diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index ac24562b1..98590974a 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -22,7 +22,7 @@ import zipfile -def ExtractR8Rules(jar, output): +def ExtractR8Rules(jar, output, r8_version): """Extract R8 rules from META-INF/com.android.tools/ inside a JAR. Handles subdirectories like r8-from-X-upto-Y/. All matching files are @@ -39,7 +39,7 @@ def ExtractR8Rules(jar, output): output.write(jar.read(entry)) -def ExtractEmbeddedProguardFromJar(jar, output): +def ExtractEmbeddedProguardFromJar(jar, output, r8_version): """Extract proguard specs from a JAR file. Reads both legacy META-INF/proguard/ and R8-targeted @@ -60,7 +60,7 @@ def ExtractEmbeddedProguardFromJar(jar, output): output.write(jar.read(entry)) -def ExtractEmbeddedProguardFromAar(aar, output): +def ExtractEmbeddedProguardFromAar(aar, output, r8_version): """Extract proguard specs from an AAR file. Reads proguard.txt from the AAR root, and also extracts R8 rules @@ -79,10 +79,10 @@ def ExtractEmbeddedProguardFromAar(aar, output): # 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) + ExtractR8Rules(jar, output, r8_version) -def ExtractEmbeddedProguardFromAarLegacy(aar, output): +def ExtractEmbeddedProguardFromAarLegacy(aar, output, r8_version): """Extract proguard specs from an AAR file (legacy behavior). Only reads proguard.txt from the AAR root. Does not extract R8 rules From 02afdd972c8d94f1e7a6608fc1a10114911651b2 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 10:36:13 +0200 Subject: [PATCH 3/9] Filter JAR R8 rules by version range, fall back to legacy ExtractEmbeddedProguardFromJar now matches r8-from-X-upto-Y directories against the actual R8 version and only extracts rules whose version range covers the current R8 version. Falls back to legacy META-INF/proguard/ rules when no targeted range matches. --- .../jar_embedded_proguard_extractor_test.py | 83 ++++++++++++++----- tools/android/proguard_extractor_lib.py | 24 ++++-- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/tools/android/jar_embedded_proguard_extractor_test.py b/tools/android/jar_embedded_proguard_extractor_test.py index 506a5b6c6..509691c8d 100644 --- a/tools/android/jar_embedded_proguard_extractor_test.py +++ b/tools/android/jar_embedded_proguard_extractor_test.py @@ -31,7 +31,7 @@ def setUp(self): def testNoProguardSpecs(self): jar = zipfile.ZipFile(io.BytesIO(), "w") proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"", proguard_file.read()) @@ -39,7 +39,7 @@ 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_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"\n-keep class A", proguard_file.read()) @@ -48,48 +48,89 @@ def testMultipleLegacyFiles(self): 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_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"\n-keep class A\n-keep class B", proguard_file.read()) - def testR8Rules(self): + def testTargetedR8RulesMatchingVersion(self): jar = zipfile.ZipFile(io.BytesIO(), "w") - jar.writestr("META-INF/com.android.tools/r8/rules.pro", "-keep class C") + jar.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class C", + ) proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"\n-keep class C", proguard_file.read()) - def testR8RulesVersionedSubdirs(self): + def testTargetedR8RulesNotMatchingVersion(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr( + "META-INF/com.android.tools/r8-from-1.0.0-upto-2.0.0/rules.pro", + "-keep class C", + ) + jar.writestr("META-INF/proguard/rules.pro", "-keep class legacy") + proguard_file = io.BytesIO() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"\n-keep class legacy", proguard_file.read()) + + def testTargetedR8RulesPreferredOverLegacy(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class targeted", + ) + jar.writestr("META-INF/proguard/rules.pro", "-keep class legacy") + proguard_file = io.BytesIO() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"\n-keep class targeted", proguard_file.read()) + + def testVersionAtLowerBoundInclusive(self): jar = zipfile.ZipFile(io.BytesIO(), "w") jar.writestr( - "META-INF/com.android.tools/r8-from-8.0.0/rules.pro", "-keep class D" + "META-INF/com.android.tools/r8-from-8.9.35-upto-9.0.0/rules.pro", + "-keep class exact", ) + proguard_file = io.BytesIO() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"\n-keep class exact", proguard_file.read()) + + def testVersionAtUpperBoundExclusive(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") jar.writestr( - "META-INF/com.android.tools/r8-upto-8.0.0/rules.pro", "-keep class E" + "META-INF/com.android.tools/r8-from-8.0.0-upto-8.9.35/rules.pro", + "-keep class excluded", ) + jar.writestr("META-INF/proguard/rules.pro", "-keep class legacy") proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) - self.assertEqual(b"\n-keep class D\n-keep class E", proguard_file.read()) + self.assertEqual(b"\n-keep class legacy", proguard_file.read()) - def testLegacyAndR8RulesCombined(self): + def testMultipleVersionedDirsOnlyMatchingIncluded(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") + jar.writestr( + "META-INF/com.android.tools/r8-from-1.0.0-upto-2.0.0/rules.pro", + "-keep class old", + ) + jar.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class current", + ) proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) - self.assertEqual(b"\n-keep class G\n-keep class F", proguard_file.read()) + self.assertEqual(b"\n-keep class current", 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") + jar.writestr("META-INF/proguard/rules.pro", "-keep class H") proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"\n-keep class H", proguard_file.read()) @@ -99,7 +140,7 @@ def testIgnoresUnrelatedMetaInf(self): 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_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"", proguard_file.read()) diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index 98590974a..b9ae79bef 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -19,9 +19,14 @@ from __future__ import print_function import io +import re import zipfile +def _parse_version(ver:str): + return tuple(int(x) for x in ver.split(".")) + + def ExtractR8Rules(jar, output, r8_version): """Extract R8 rules from META-INF/com.android.tools/ inside a JAR. @@ -52,12 +57,21 @@ def ExtractEmbeddedProguardFromJar(jar, output, r8_version): legacy_prefix = "META-INF/proguard/" r8_prefix = "META-INF/com.android.tools/" + dirs_with_targeted_r8_rules = [] + legacy_rules = [] 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)) + if entry.startswith(r8_prefix) and re.match("r8-from-[^/]+-upto-[^/]+", entry[len(r8_prefix):]): + ver_bounds = re.search("r8-from-([^/]+)-upto-([^/]+)", entry) + if ver_bounds and (_parse_version(ver_bounds.group(1)) <= _parse_version(r8_version) < _parse_version(ver_bounds.group(2))): + dirs_with_targeted_r8_rules.append(entry) + + if entry.startswith(legacy_prefix) and not entry.endswith("/"): + legacy_rules.append(entry) + + + for out_entry in (dirs_with_targeted_r8_rules or legacy_rules): + output.write(b"\n") + output.write(jar.read(out_entry)) def ExtractEmbeddedProguardFromAar(aar, output, r8_version): From 391cf34daa99db5d6b5f3bd0210b0d5b9b6d3d5d Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 10:48:03 +0200 Subject: [PATCH 4/9] Filter AAR R8 rules by version range, fall back to proguard.txt Update ExtractEmbeddedProguardFromAar to prioritize version-targeted R8 rules from classes.jar, falling back to proguard.txt at the AAR root when no targeted range matches. Remove unused ExtractR8Rules and ExtractEmbeddedProguardFromAarLegacy functions. --- .../aar_embedded_proguard_extractor_test.py | 113 +++++------------- tools/android/proguard_extractor_lib.py | 76 +++++------- 2 files changed, 61 insertions(+), 128 deletions(-) diff --git a/tools/android/aar_embedded_proguard_extractor_test.py b/tools/android/aar_embedded_proguard_extractor_test.py index 440f872e2..16114cc7b 100644 --- a/tools/android/aar_embedded_proguard_extractor_test.py +++ b/tools/android/aar_embedded_proguard_extractor_test.py @@ -18,59 +18,14 @@ import unittest import zipfile -from tools.android import aar_embedded_proguard_extractor from tools.android import proguard_extractor_lib -class AarEmbeddedProguardExtractorLegacyTest(unittest.TestCase): - """Unit tests for AAR proguard extraction. - - Legacy behavior, i.e. extract_r8_rules=False. - """ +class AarEmbeddedProguardExtractorTest(unittest.TestCase): + """Unit tests for AAR proguard extraction.""" def setUp(self): - super(AarEmbeddedProguardExtractorLegacyTest, 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()) - - def make_classes_jar(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.make_classes_jar({ - "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() + super(AarEmbeddedProguardExtractorTest, self).setUp() os.chdir(os.environ["TEST_TMPDIR"]) def make_classes_jar(self, entries): @@ -83,7 +38,7 @@ def make_classes_jar(self, entries): def testNoProguardTxt(self): aar = zipfile.ZipFile(io.BytesIO(), "w") proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"", proguard_file.read()) @@ -91,74 +46,70 @@ 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_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"hello world", proguard_file.read()) - def testR8RulesFromClassesJar(self): + def testTargetedR8RulesFromClassesJar(self): classes_jar = self.make_classes_jar({ - "META-INF/com.android.tools/r8/rules.pro": "-keep class A", + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/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_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") proguard_file.seek(0) self.assertEqual(b"\n-keep class A", proguard_file.read()) - def testR8RulesFromVersionedSubdirs(self): + def testTargetedR8RulesPreferredOverProguardTxt(self): classes_jar = self.make_classes_jar({ - "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", + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro": "-keep class targeted", }) aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("proguard.txt", "-keep class legacy") aar.writestr("classes.jar", classes_jar) proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") proguard_file.seek(0) - self.assertEqual(b"\n-keep class B\n-keep class C", proguard_file.read()) + self.assertEqual(b"\n-keep class targeted", proguard_file.read()) - def testR8RulesAndProguardTxtCombined(self): + def testFallsBackToProguardTxtWhenNoVersionMatch(self): classes_jar = self.make_classes_jar({ - "META-INF/com.android.tools/r8/rules.pro": "-keep class D", + "META-INF/com.android.tools/r8-from-1.0.0-upto-2.0.0/rules.pro": "-keep class old", }) aar = zipfile.ZipFile(io.BytesIO(), "w") - aar.writestr("proguard.txt", "-keep class E") + aar.writestr("proguard.txt", "-keep class legacy") aar.writestr("classes.jar", classes_jar) proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") proguard_file.seek(0) - self.assertEqual(b"-keep class E\n-keep class D", proguard_file.read()) + self.assertEqual(b"-keep class legacy", proguard_file.read()) - def testR8RulesIgnoresDirectoryEntries(self): - classes_jar = self.make_classes_jar({ - "META-INF/com.android.tools/": "", - "META-INF/com.android.tools/r8/": "", - "META-INF/com.android.tools/r8/rules.pro": "-keep class F", - }) + def testNoClassesJarFallsBackToProguardTxt(self): aar = zipfile.ZipFile(io.BytesIO(), "w") - aar.writestr("classes.jar", classes_jar) + aar.writestr("proguard.txt", "-keep class legacy") proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") 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()) + self.assertEqual(b"-keep class legacy", proguard_file.read()) def testClassesJarWithoutR8Rules(self): classes_jar = self.make_classes_jar({ "com/example/Foo.class": "classdata", }) aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("proguard.txt", "-keep class legacy") aar.writestr("classes.jar", classes_jar) proguard_file = io.BytesIO() - proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file) + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"-keep class legacy", proguard_file.read()) + + def testNoClassesJarNoProguardTxt(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, "8.9.35") proguard_file.seek(0) self.assertEqual(b"", proguard_file.read()) diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index b9ae79bef..1e53384fe 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -27,51 +27,45 @@ def _parse_version(ver:str): return tuple(int(x) for x in ver.split(".")) -def ExtractR8Rules(jar, output, r8_version): - """Extract R8 rules from META-INF/com.android.tools/ inside a JAR. +def _ExtractTargetedR8Rules(jar, output, r8_version): + """Extract version-targeted R8 rules from META-INF/com.android.tools/. - Handles subdirectories like r8-from-X-upto-Y/. All matching files are - concatenated into the output, sorted by path for determinism. - - Args: - jar: The JAR file to extract from. - output: The output file to write to. + Returns True if any matching rules were found and written. """ - meta_inf_prefix = "META-INF/com.android.tools/" + r8_prefix = "META-INF/com.android.tools/" + + targeted_entries = [] 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)) + if entry.startswith(r8_prefix) and re.match("r8-from-[^/]+-upto-[^/]+", entry[len(r8_prefix):]): + ver_bounds = re.search("r8-from-([^/]+)-upto-([^/]+)", entry) + if ver_bounds and (_parse_version(ver_bounds.group(1)) <= _parse_version(r8_version) < _parse_version(ver_bounds.group(2))): + targeted_entries.append(entry) + + for out_entry in targeted_entries: + output.write(b"\n") + output.write(jar.read(out_entry)) def ExtractEmbeddedProguardFromJar(jar, output, r8_version): """Extract proguard specs from a JAR file. - Reads both legacy META-INF/proguard/ and R8-targeted - META-INF/com.android.tools/ entries. + Extracts version-targeted R8 rules from META-INF/com.android.tools/. + Falls back to legacy META-INF/proguard/ if no targeted rules match. Args: jar: The JAR file to extract from. output: The output file to write to. """ - legacy_prefix = "META-INF/proguard/" - r8_prefix = "META-INF/com.android.tools/" + pos = output.tell() + _ExtractTargetedR8Rules(jar, output, r8_version) + if output.tell() > pos: + return - dirs_with_targeted_r8_rules = [] - legacy_rules = [] + legacy_prefix = "META-INF/proguard/" for entry in sorted(jar.namelist()): - if entry.startswith(r8_prefix) and re.match("r8-from-[^/]+-upto-[^/]+", entry[len(r8_prefix):]): - ver_bounds = re.search("r8-from-([^/]+)-upto-([^/]+)", entry) - if ver_bounds and (_parse_version(ver_bounds.group(1)) <= _parse_version(r8_version) < _parse_version(ver_bounds.group(2))): - dirs_with_targeted_r8_rules.append(entry) - if entry.startswith(legacy_prefix) and not entry.endswith("/"): - legacy_rules.append(entry) - - - for out_entry in (dirs_with_targeted_r8_rules or legacy_rules): - output.write(b"\n") - output.write(jar.read(out_entry)) + output.write(b"\n") + output.write(jar.read(entry)) def ExtractEmbeddedProguardFromAar(aar, output, r8_version): @@ -87,26 +81,14 @@ def ExtractEmbeddedProguardFromAar(aar, output, r8_version): 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 + # Try targeted R8 rules from classes.jar first if classes_jar in aar.namelist(): with zipfile.ZipFile(io.BytesIO(aar.read(classes_jar)), "r") as jar: - ExtractR8Rules(jar, output, r8_version) - - -def ExtractEmbeddedProguardFromAarLegacy(aar, output, r8_version): - """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. - - Args: - aar: The AAR file to extract from. - output: The output file to write to. - """ - proguard_spec = "proguard.txt" + pos = output.tell() + _ExtractTargetedR8Rules(jar, output, r8_version) + if output.tell() > pos: + return + # Fall back to legacy proguard.txt if proguard_spec in aar.namelist(): output.write(aar.read(proguard_spec)) From 7b7c6b0cc980218f39b0c899a8e2cf1b249035a0 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 11:23:16 +0200 Subject: [PATCH 5/9] Handle the case, when r8_version is missing --- .../android/aar_embedded_proguard_extractor_test.py | 12 ++++++++++++ .../android/jar_embedded_proguard_extractor_test.py | 13 +++++++++++++ tools/android/proguard_extractor_lib.py | 3 +++ 3 files changed, 28 insertions(+) diff --git a/tools/android/aar_embedded_proguard_extractor_test.py b/tools/android/aar_embedded_proguard_extractor_test.py index 16114cc7b..b39d29e1a 100644 --- a/tools/android/aar_embedded_proguard_extractor_test.py +++ b/tools/android/aar_embedded_proguard_extractor_test.py @@ -105,6 +105,18 @@ def testClassesJarWithoutR8Rules(self): proguard_file.seek(0) self.assertEqual(b"-keep class legacy", proguard_file.read()) + def testNoneR8VersionFallsBackToProguardTxt(self): + classes_jar = self.make_classes_jar({ + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro": "-keep class targeted", + }) + aar = zipfile.ZipFile(io.BytesIO(), "w") + aar.writestr("proguard.txt", "-keep class legacy") + aar.writestr("classes.jar", classes_jar) + proguard_file = io.BytesIO() + proguard_extractor_lib.ExtractEmbeddedProguardFromAar(aar, proguard_file, None) + proguard_file.seek(0) + self.assertEqual(b"-keep class legacy", proguard_file.read()) + def testNoClassesJarNoProguardTxt(self): aar = zipfile.ZipFile(io.BytesIO(), "w") aar.writestr("some_other_file.txt", "data") diff --git a/tools/android/jar_embedded_proguard_extractor_test.py b/tools/android/jar_embedded_proguard_extractor_test.py index 509691c8d..f70b69d88 100644 --- a/tools/android/jar_embedded_proguard_extractor_test.py +++ b/tools/android/jar_embedded_proguard_extractor_test.py @@ -145,5 +145,18 @@ def testIgnoresUnrelatedMetaInf(self): self.assertEqual(b"", proguard_file.read()) + def testNoneR8VersionFallsBackToLegacy(self): + jar = zipfile.ZipFile(io.BytesIO(), "w") + jar.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class targeted", + ) + jar.writestr("META-INF/proguard/rules.pro", "-keep class legacy") + proguard_file = io.BytesIO() + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, None) + proguard_file.seek(0) + self.assertEqual(b"\n-keep class legacy", proguard_file.read()) + + if __name__ == "__main__": unittest.main() diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index 1e53384fe..53f5154c4 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -32,6 +32,9 @@ def _ExtractTargetedR8Rules(jar, output, r8_version): Returns True if any matching rules were found and written. """ + if not r8_version: + return + r8_prefix = "META-INF/com.android.tools/" targeted_entries = [] From 962b5e9945af7d7540377a6f1daf40c8ad7a6cbb Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 13:36:14 +0200 Subject: [PATCH 6/9] Make bound checking a bit more readable --- tools/android/proguard_extractor_lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index 53f5154c4..ad405793d 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -40,9 +40,11 @@ def _ExtractTargetedR8Rules(jar, output, r8_version): targeted_entries = [] for entry in sorted(jar.namelist()): if entry.startswith(r8_prefix) and re.match("r8-from-[^/]+-upto-[^/]+", entry[len(r8_prefix):]): - ver_bounds = re.search("r8-from-([^/]+)-upto-([^/]+)", entry) - if ver_bounds and (_parse_version(ver_bounds.group(1)) <= _parse_version(r8_version) < _parse_version(ver_bounds.group(2))): - targeted_entries.append(entry) + match = re.search("r8-from-([^/]+)-upto-([^/]+)", entry) + if match: + lower_bound, upper_bound = match.groups() + if _parse_version(lower_bound) <= _parse_version(r8_version) < _parse_version(upper_bound): + targeted_entries.append(entry) for out_entry in targeted_entries: output.write(b"\n") From 4c613228d4979bae8dc43c8e871c9f9e61331e27 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 16:18:31 +0200 Subject: [PATCH 7/9] Fix the stale comment --- tools/android/proguard_extractor_lib.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/android/proguard_extractor_lib.py b/tools/android/proguard_extractor_lib.py index ad405793d..a6ae1e836 100644 --- a/tools/android/proguard_extractor_lib.py +++ b/tools/android/proguard_extractor_lib.py @@ -28,10 +28,7 @@ def _parse_version(ver:str): def _ExtractTargetedR8Rules(jar, output, r8_version): - """Extract version-targeted R8 rules from META-INF/com.android.tools/. - - Returns True if any matching rules were found and written. - """ + """Extract version-targeted R8 rules from META-INF/com.android.tools/.""" if not r8_version: return From a53a9bfe00dc9d2bfac0106be48b42dac9c601b0 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Fri, 22 May 2026 22:33:47 +0200 Subject: [PATCH 8/9] Extract rules from each jar --- rules/android_binary/r8.bzl | 21 ++++++----- .../jar_embedded_proguard_extractor.py | 36 ++++++++----------- .../jar_embedded_proguard_extractor_test.py | 35 ++++++++++++++++++ 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/rules/android_binary/r8.bzl b/rules/android_binary/r8.bzl index 2516c881c..6c04f6ea6 100644 --- a/rules/android_binary/r8.bzl +++ b/rules/android_binary/r8.bzl @@ -64,14 +64,16 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ # The deploy jar from the deploy_jar processor is not used because as of now, whether it # actually produces a deploy jar is determinted by a separate set of ACLs, and also does # desugaring differently than with R8. + runtime_jars = depset( + direct = jvm_ctx.java_info.runtime_output_jars + [packaged_resources_ctx.class_jar], + transitive = [jvm_ctx.java_info.transitive_runtime_jars], + ) + deploy_jar = ctx.actions.declare_file(ctx.label.name + "_deploy.jar") java.create_deploy_jar( ctx, output = deploy_jar, - runtime_jars = depset( - direct = jvm_ctx.java_info.runtime_output_jars + [packaged_resources_ctx.class_jar], - transitive = [jvm_ctx.java_info.transitive_runtime_jars], - ), + runtime_jars = runtime_jars, java_toolchain = common.get_java_toolchain(ctx), build_target = ctx.label.name, deploy_manifest_lines = build_info_ctx.deploy_manifest_lines, @@ -84,20 +86,21 @@ def process_r8(ctx, validation_ctx, jvm_ctx, packaged_resources_ctx, build_info_ proguard_specs = proguard.get_proguard_specs(ctx, packaged_resources_ctx.resource_proguard_config) desugared_lib_config = ctx.file._desugared_lib_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. + # Optionally extract proguard specs embedded in runtime JARs (META-INF/proguard/ + # and META-INF/com.android.tools/) so they are passed to R8. Each JAR is processed + # independently to avoid META-INF clobbering from the deploy JAR merge. 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_all("--input_jars", runtime_jars) 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], + inputs = runtime_jars, outputs = [jar_embedded_proguard], mnemonic = "JarEmbeddedProguardExtractor", - progress_message = "Extracting proguard specs from deploy jar for %{label}", + progress_message = "Extracting proguard specs from runtime jars for %{label}", toolchain = None, ) proguard_specs = proguard_specs + [jar_embedded_proguard] diff --git a/tools/android/jar_embedded_proguard_extractor.py b/tools/android/jar_embedded_proguard_extractor.py index b10a7fdf7..28ace378c 100644 --- a/tools/android/jar_embedded_proguard_extractor.py +++ b/tools/android/jar_embedded_proguard_extractor.py @@ -33,23 +33,19 @@ FLAGS = flags.FLAGS -flags.DEFINE_string("input_jar", None, "Input JAR") -flags.mark_flag_as_required("input_jar") +flags.DEFINE_multi_string("input_jars", None, "Input JAR(s)") +flags.mark_flag_as_required("input_jars") flags.DEFINE_string( "output_proguard_file", None, "Output parameter file for proguard" ) flags.mark_flag_as_required("output_proguard_file") -def ExtractEmbeddedProguard(jar, output, r8_version): - """Extract proguard specs from a JAR file.""" - proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, output, r8_version) - - -def _Main(input_jar, output_proguard_file, r8_version = None): - with zipfile.ZipFile(input_jar, "r") as jar: - with open(output_proguard_file, "wb") as output: - ExtractEmbeddedProguard(jar, output, r8_version) +def _Main(input_jars, output_proguard_file, r8_version = None): + with open(output_proguard_file, "wb") as output: + for input_jar in input_jars: + with zipfile.ZipFile(input_jar, "r") as jar: + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, output, r8_version) def main(unused_argv): @@ -61,20 +57,16 @@ def main(unused_argv): r8_version = runfile_lines[0].strip() 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)), - r8_version - ) + with junction.TempJunction(os.path.dirname(proguard_long)) as proguard_junc: + _Main( + [os.path.abspath(j) for j in FLAGS.input_jars], + os.path.join(proguard_junc, os.path.basename(proguard_long)), + r8_version, + ) else: - _Main(FLAGS.input_jar, FLAGS.output_proguard_file, r8_version) + _Main(FLAGS.input_jars, FLAGS.output_proguard_file, r8_version) if __name__ == "__main__": diff --git a/tools/android/jar_embedded_proguard_extractor_test.py b/tools/android/jar_embedded_proguard_extractor_test.py index f70b69d88..c84406d9a 100644 --- a/tools/android/jar_embedded_proguard_extractor_test.py +++ b/tools/android/jar_embedded_proguard_extractor_test.py @@ -157,6 +157,41 @@ def testNoneR8VersionFallsBackToLegacy(self): proguard_file.seek(0) self.assertEqual(b"\n-keep class legacy", proguard_file.read()) + def testMultipleJarsExtractedIndependently(self): + jar1 = zipfile.ZipFile(io.BytesIO(), "w") + jar1.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class A", + ) + + jar2 = zipfile.ZipFile(io.BytesIO(), "w") + jar2.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class B", + ) + + proguard_file = io.BytesIO() + for jar in [jar1, jar2]: + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"\n-keep class A\n-keep class B", proguard_file.read()) + + def testMultipleJarsIndependentFallback(self): + jar1 = zipfile.ZipFile(io.BytesIO(), "w") + jar1.writestr( + "META-INF/com.android.tools/r8-from-8.0.0-upto-9.0.0/rules.pro", + "-keep class targeted", + ) + + jar2 = zipfile.ZipFile(io.BytesIO(), "w") + jar2.writestr("META-INF/proguard/rules.pro", "-keep class legacy") + + proguard_file = io.BytesIO() + for jar in [jar1, jar2]: + proguard_extractor_lib.ExtractEmbeddedProguardFromJar(jar, proguard_file, "8.9.35") + proguard_file.seek(0) + self.assertEqual(b"\n-keep class targeted\n-keep class legacy", proguard_file.read()) + if __name__ == "__main__": unittest.main() From 5e30c57ddd96ae97b58b89958c3236c59bac1d83 Mon Sep 17 00:00:00 2001 From: Sheroz Nazhmudinov Date: Sun, 24 May 2026 13:19:01 +0200 Subject: [PATCH 9/9] Create R8Version extractor; read the R8 versionf from jar's Version.java class --- tools/android/BUILD | 11 +++++++++-- tools/android/R8Version.java | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tools/android/R8Version.java diff --git a/tools/android/BUILD b/tools/android/BUILD index 79edef5d9..22b12576c 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -266,6 +266,13 @@ java_binary( runtime_deps = ["@rules_android_maven//:com_android_tools_r8"], ) +java_binary( + name = "r8_version_bin", + srcs = ["R8Version.java"], + main_class = "tools.android.R8Version", + deps = ["@rules_android_maven//:com_android_tools_r8"], +) + java_binary( name = "tracereferences", jvm_flags = _JVM_FLAGS, @@ -670,6 +677,6 @@ genrule( outs = [ "r8.version", ], - cmd = "$(location :r8) --version | awk -F' ' '{ print $$2 }' >$(OUTS)", - tools = [":r8"], + cmd = "$(location :r8_version_bin) >$(OUTS)", + tools = [":r8_version_bin"], ) diff --git a/tools/android/R8Version.java b/tools/android/R8Version.java new file mode 100644 index 000000000..a5d8d6464 --- /dev/null +++ b/tools/android/R8Version.java @@ -0,0 +1,7 @@ +package tools.android; + +public class R8Version { + public static void main(String[] args) { + System.out.println(com.android.tools.r8.Version.LABEL); + } +}