Merge commit 'bb8608b7b0e6059678e2b7dcfcb7b9aba561cfa0' into dev-release
diff --git a/.gitignore b/.gitignore
index 018c71b..a71b445 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,6 +165,8 @@
third_party/openjdk/desugar_jdk_libs_releases/1.1.1.tar.gz
third_party/openjdk/desugar_jdk_libs_releases/1.1.5
third_party/openjdk/desugar_jdk_libs_releases/1.1.5.tar.gz
+third_party/openjdk/desugar_jdk_libs_releases/2.0.3
+third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz
third_party/openjdk/jdk-[0-9][0-9]/linux
third_party/openjdk/jdk-[0-9][0-9]/linux.tar.gz
third_party/openjdk/jdk-[0-9][0-9]/osx
@@ -296,8 +298,8 @@
tools/*/host/art-12.0.0-beta4.tar.gz
tools/*/host/art-13.0.0
tools/*/host/art-13.0.0.tar.gz
-tools/*/host/art-14.0.0-dp1
-tools/*/host/art-14.0.0-dp1.tar.gz
+tools/*/host/art-14.0.0-beta3
+tools/*/host/art-14.0.0-beta3.tar.gz
tools/*/host/art-master
tools/*/host/art-master.tar.gz
tools/*/art.tar.gz
diff --git a/build.gradle b/build.gradle
index 04704bc..4b948af 100644
--- a/build.gradle
+++ b/build.gradle
@@ -345,12 +345,12 @@
"openjdk/openjdk-rt-1.8",
"openjdk/desugar_jdk_libs",
"openjdk/desugar_jdk_libs_11",
- "openjdk/desugar_jdk_libs_legacy",
"openjdk/desugar_jdk_libs_releases/1.0.9",
"openjdk/desugar_jdk_libs_releases/1.0.10",
"openjdk/desugar_jdk_libs_releases/1.1.0",
"openjdk/desugar_jdk_libs_releases/1.1.1",
"openjdk/desugar_jdk_libs_releases/1.1.5",
+ "openjdk/desugar_jdk_libs_releases/2.0.3",
"openjdk/jdk-11-test",
"opensource-apps/tivi",
"proguard/proguard5.2.1",
@@ -377,7 +377,7 @@
"linux/art-10.0.0",
"linux/host/art-12.0.0-beta4",
"linux/host/art-13.0.0",
- "linux/host/art-14.0.0-dp1",
+ "linux/host/art-14.0.0-beta3",
"linux/host/art-master",
"linux/dalvik",
"linux/dalvik-4.0.4",
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index a04d4f1..49e79b5 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -22,6 +22,7 @@
dependencies {
implementation(":keepanno")
+ implementation(":resourceshrinker")
compileOnly(Deps.asm)
compileOnly(Deps.asmCommons)
compileOnly(Deps.asmUtil)
@@ -37,6 +38,8 @@
listOf(ThirdPartyDeps.apiDatabase))
val keepAnnoJarTask = projectTask("keepanno", "jar")
+val resourceShrinkerJarTask = projectTask("resourceshrinker", "jar")
+val resourceShrinkerDepsTask = projectTask("resourceshrinker", "depsJar")
fun mainJarDependencies() : FileCollection {
return sourceSets
@@ -75,9 +78,13 @@
doFirst {
println(header("R8 full dependencies"))
}
+ dependsOn(resourceShrinkerJarTask)
+ dependsOn(resourceShrinkerDepsTask)
mainJarDependencies().forEach({ println(it) })
from(mainJarDependencies().map(::zipTree))
from(keepAnnoJarTask.outputs.files.map(::zipTree))
+ from(resourceShrinkerJarTask.outputs.files.map(::zipTree))
+ from(resourceShrinkerDepsTask.outputs.files.map(::zipTree))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveFileName.set("deps.jar")
}
diff --git a/d8_r8/main/settings.gradle.kts b/d8_r8/main/settings.gradle.kts
index 765e0f8..019fbf2 100644
--- a/d8_r8/main/settings.gradle.kts
+++ b/d8_r8/main/settings.gradle.kts
@@ -5,4 +5,5 @@
rootProject.name = "r8"
val root = rootProject.projectDir.parentFile
-includeBuild(root.resolve("keepanno"))
\ No newline at end of file
+includeBuild(root.resolve("keepanno"))
+includeBuild(root.resolve("resourceshrinker"))
diff --git a/d8_r8/r8lib/build.gradle.kts b/d8_r8/r8lib/build.gradle.kts
index c3eea8f..2e4bfa7 100644
--- a/d8_r8/r8lib/build.gradle.kts
+++ b/d8_r8/r8lib/build.gradle.kts
@@ -66,14 +66,17 @@
dependsOn(r8WithRelocatedDepsTask)
val r8 = r8WithRelocatedDepsTask.outputs.files.getSingleFile()
val generatedKeepRules = generateKeepRules.get().outputs.files.getSingleFile()
- inputs.files(listOf(r8, generatedKeepRules))
+ val keepTxt = getRoot().resolveAll("src", "main", "keep.txt")
+ // TODO(b/294351878): Remove once enum issue is fixed
+ val keepResourceShrinkerTxt = getRoot().resolveAll("src", "main", "keep_r8resourceshrinker.txt")
+ inputs.files(listOf(r8, generatedKeepRules, keepTxt, keepResourceShrinkerTxt))
val output = file(Paths.get("build", "libs", "r8lib-deps-relocated.jar"))
outputs.file(output)
commandLine = createR8LibCommandLine(
r8,
r8,
output,
- listOf(getRoot().resolveAll("src", "main", "keep.txt"), generatedKeepRules),
+ listOf(keepTxt, generatedKeepRules, keepResourceShrinkerTxt),
false)
}
@@ -94,4 +97,19 @@
true,
listOf(deps))
}
+
+ val resourceshrinkercli by registering(Exec::class) {
+ dependsOn(r8WithRelocatedDepsTask)
+ val r8 = r8WithRelocatedDepsTask.outputs.files.getSingleFile()
+ val keepTxt = getRoot().resolveAll("src", "main", "resourceshrinker_cli.txt")
+ inputs.file(keepTxt)
+ val output = file(Paths.get("build", "libs", "resourceshrinkercli.jar"))
+ outputs.file(output)
+ commandLine = createR8LibCommandLine(
+ r8,
+ r8,
+ output,
+ listOf(keepTxt),
+ false)
+ }
}
diff --git a/d8_r8/resourceshrinker/build.gradle.kts b/d8_r8/resourceshrinker/build.gradle.kts
new file mode 100644
index 0000000..e4cda0c
--- /dev/null
+++ b/d8_r8/resourceshrinker/build.gradle.kts
@@ -0,0 +1,60 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ `kotlin-dsl`
+ id("dependencies-plugin")
+}
+
+java {
+ sourceSets.main.configure {
+ kotlin.srcDir(getRoot().resolveAll("src", "resourceshrinker", "java"))
+ java.srcDir(getRoot().resolveAll("src", "resourceshrinker", "java"))
+ }
+ sourceCompatibility = JvmCompatibility.sourceCompatibility
+ targetCompatibility = JvmCompatibility.targetCompatibility
+}
+
+fun jarDependencies() : FileCollection {
+ return sourceSets
+ .main
+ .get()
+ .compileClasspath
+ .filter({ "$it".contains("third_party")
+ && "$it".contains("dependencies")
+ })
+}
+
+tasks {
+ withType<KotlinCompile> {
+ kotlinOptions {
+ // We cannot use languageVersion.set(JavaLanguageVersion.of(8)) because gradle cannot figure
+ // out that the jdk is 1_8 and will try to download it.
+ jvmTarget = "11"
+ }
+ }
+
+ val depsJar by registering(Jar::class) {
+ println(header("Resource shrinker dependencies"))
+ jarDependencies().forEach({ println(it) })
+ from(jarDependencies().map(::zipTree))
+ exclude("**/*.proto")
+ exclude("versions-offline/**")
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ archiveFileName.set("resourceshrinker_deps.jar")
+ }
+}
+
+dependencies {
+ compileOnly(Deps.asm)
+ compileOnly(Deps.guava)
+ compileOnly(files(getRoot().resolve("third_party/r8/r8lib_8.2.20-dev.jar")))
+ implementation("com.android.tools.build:aapt2-proto:8.2.0-alpha10-10154469")
+ implementation("com.google.protobuf:protobuf-java:3.19.3")
+ implementation("com.android.tools.layoutlib:layoutlib-api:31.2.0-alpha10")
+ implementation("com.android.tools:common:31.2.0-alpha10")
+ implementation("com.android.tools:sdk-common:31.2.0-alpha10")
+}
diff --git a/d8_r8/resourceshrinker/gradle.properties b/d8_r8/resourceshrinker/gradle.properties
new file mode 100644
index 0000000..1de43f9
--- /dev/null
+++ b/d8_r8/resourceshrinker/gradle.properties
@@ -0,0 +1,17 @@
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
+kotlin.daemon.jvmargs=-Xmx3g -Dkotlin.js.compiler.legacy.force_enabled=true
+systemProp.file.encoding=UTF-8
+
+# Enable new incremental compilation
+kotlin.incremental.useClasspathSnapshot=true
+
+org.gradle.parallel=true
+org.gradle.caching=true
+
+# Do not download any jdks or detect them. We provide them.
+org.gradle.java.installations.auto-detect=false
+org.gradle.java.installations.auto-download=false
diff --git a/d8_r8/resourceshrinker/settings.gradle.kts b/d8_r8/resourceshrinker/settings.gradle.kts
new file mode 100644
index 0000000..604d9cb
--- /dev/null
+++ b/d8_r8/resourceshrinker/settings.gradle.kts
@@ -0,0 +1,27 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+pluginManagement {
+ repositories {
+ maven {
+ url = uri("file:../../third_party/dependencies")
+ }
+ maven {
+ url = uri("file:../../third_party/dependencies_new")
+ }
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ maven {
+ url = uri("file:../../third_party/dependencies")
+ }
+ maven {
+ url = uri("file:../../third_party/dependencies_new")
+ }
+ }
+}
+
+rootProject.name = "resourceshrinker"
diff --git a/d8_r8/settings.gradle.kts b/d8_r8/settings.gradle.kts
index 77ad2e0..a02c904 100644
--- a/d8_r8/settings.gradle.kts
+++ b/d8_r8/settings.gradle.kts
@@ -50,6 +50,7 @@
// This project is temporarily located in d8_r8. When moved to root, the parent
// folder should just be removed.
includeBuild(root.resolve("keepanno"))
+includeBuild(root.resolve("resourceshrinker"))
// We need to include src/main as a composite-build otherwise our test-modules
// will compete with the test to compile the source files.
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 5c3f1b9..388f09b 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -142,42 +142,6 @@
}
}
builders {
- name: "desugared_library-head"
- swarming_host: "chrome-swarming.appspot.com"
- swarming_tags: "vpython:native-python-wrapper"
- dimensions: "cores:8"
- dimensions: "cpu:x86-64"
- dimensions: "os:Ubuntu-20.04"
- dimensions: "pool:luci.r8.ci"
- exe {
- cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
- cipd_version: "refs/heads/master"
- cmd: "luciexe"
- }
- properties:
- '{'
- ' "builder_group": "internal.client.r8",'
- ' "recipe": "rex",'
- ' "test_options": ['
- ' "--one_line_per_test",'
- ' "--archive_failures",'
- ' "--no_internal",'
- ' "--no_arttests",'
- ' "--desugared-library",'
- ' "HEAD"'
- ' ]'
- '}'
- priority: 26
- execution_timeout_secs: 43200
- expiration_secs: 126000
- build_numbers: YES
- service_account: "r8-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
- experiments {
- key: "luci.recipes.use_python3"
- value: 100
- }
- }
- builders {
name: "desugared_library-jdk11_head"
swarming_host: "chrome-swarming.appspot.com"
swarming_tags: "vpython:native-python-wrapper"
diff --git a/infra/config/global/generated/luci-milo.cfg b/infra/config/global/generated/luci-milo.cfg
index b0f2546..8d17a6a 100644
--- a/infra/config/global/generated/luci-milo.cfg
+++ b/infra/config/global/generated/luci-milo.cfg
@@ -151,11 +151,6 @@
short_name: "jdk8"
}
builders {
- name: "buildbucket/luci.r8.ci/desugared_library-head"
- category: "library_desugar"
- short_name: "head"
- }
- builders {
name: "buildbucket/luci.r8.ci/desugared_library-jdk11_head"
category: "library_desugar"
short_name: "jdk11_head"
diff --git a/infra/config/global/generated/luci-notify.cfg b/infra/config/global/generated/luci-notify.cfg
index 048dc7a..d0fb763 100644
--- a/infra/config/global/generated/luci-notify.cfg
+++ b/infra/config/global/generated/luci-notify.cfg
@@ -60,18 +60,6 @@
}
builders {
bucket: "ci"
- name: "desugared_library-head"
- repository: "https://r8.googlesource.com/r8"
- }
-}
-notifiers {
- notifications {
- on_failure: true
- on_new_failure: true
- notify_blamelist {}
- }
- builders {
- bucket: "ci"
name: "desugared_library-jdk11_head"
repository: "https://r8.googlesource.com/r8"
}
diff --git a/infra/config/global/generated/luci-scheduler.cfg b/infra/config/global/generated/luci-scheduler.cfg
index 91a12e2..faedc8a 100644
--- a/infra/config/global/generated/luci-scheduler.cfg
+++ b/infra/config/global/generated/luci-scheduler.cfg
@@ -65,20 +65,6 @@
}
}
job {
- id: "desugared_library-head"
- realm: "ci"
- acl_sets: "ci"
- triggering_policy {
- kind: GREEDY_BATCHING
- max_concurrent_invocations: 4
- }
- buildbucket {
- server: "cr-buildbucket.appspot.com"
- bucket: "ci"
- builder: "desugared_library-head"
- }
-}
-job {
id: "desugared_library-jdk11_head"
realm: "ci"
acl_sets: "ci"
@@ -829,7 +815,6 @@
triggers: "archive"
triggers: "cached"
triggers: "check"
- triggers: "desugared_library-head"
triggers: "desugared_library-jdk11_head"
triggers: "linux-android-10.0.0"
triggers: "linux-android-12.0.0"
diff --git a/infra/config/global/generated/project.cfg b/infra/config/global/generated/project.cfg
index ee2ef9d..c275232 100644
--- a/infra/config/global/generated/project.cfg
+++ b/infra/config/global/generated/project.cfg
@@ -7,7 +7,7 @@
name: "r8"
access: "group:all"
lucicfg {
- version: "1.39.8"
+ version: "1.39.11"
package_dir: ".."
config_dir: "generated"
entry_point: "main.star"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index dd14d77..520bb77 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -400,30 +400,28 @@
app_dump()
def desugared_library():
- for name in ["head", "jdk11_head"]:
- test_options = [
- "--one_line_per_test",
- "--archive_failures",
- "--no_internal",
- "--no_arttests",
- "--desugared-library",
- "HEAD"
- ]
- if "jdk11" in name:
- test_options = test_options + ["--desugared-library-configuration", "jdk11"]
- properties = {
- "builder_group" : "internal.client.r8",
- "test_options" : test_options,
- }
- name = "desugared_library-" + name
- r8_builder(
- name,
- category = "library_desugar",
- dimensions = get_dimensions(),
- execution_timeout = time.hour * 12,
- expiration_timeout = time.hour * 35,
- properties = properties,
- )
+ test_options = [
+ "--one_line_per_test",
+ "--archive_failures",
+ "--no_internal",
+ "--no_arttests",
+ "--desugared-library",
+ "HEAD",
+ "--desugared-library-configuration",
+ "jdk11"
+ ]
+ properties = {
+ "builder_group" : "internal.client.r8",
+ "test_options" : test_options,
+ }
+ r8_builder(
+ "desugared_library-jdk11_head",
+ category = "library_desugar",
+ dimensions = get_dimensions(),
+ execution_timeout = time.hour * 12,
+ expiration_timeout = time.hour * 35,
+ properties = properties,
+ )
desugared_library()
r8_builder(
diff --git a/scripts/add-android-jar.sh b/scripts/add-android-jar.sh
index eabdedc..e6ba2c2 100755
--- a/scripts/add-android-jar.sh
+++ b/scripts/add-android-jar.sh
@@ -16,8 +16,8 @@
SDK_HOME=$HOME/Android/Sdk
# Modify these to match the SDK android.jar to add.
-SDK_DIR_NAME=android-33
-SDK_VERSION=33
+SDK_DIR_NAME=android-UpsideDownCake
+SDK_VERSION=34
SDK_DIR=$SDK_HOME/platforms/$SDK_DIR_NAME
THIRD_PARTY_ANDROID_JAR=third_party/android_jar
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index 05b48f0..965e3cf 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -140,6 +140,7 @@
"api_level_below_or_equal": 25,
"rewrite_prefix": {
"java.time.": "j$.time.",
+ "java.util.Base64": "j$.util.Base64",
"java.util.Desugar": "j$.util.Desugar"
},
"dont_rewrite_prefix": [
@@ -267,6 +268,12 @@
"java.util.OptionalInt": "java.util.OptionalConversions",
"java.util.OptionalLong": "java.util.OptionalConversions"
}
+ },
+ {
+ "api_level_below_or_equal": 18,
+ "rewrite_prefix": {
+ "java.nio.charset.StandardCharsets": "j$.nio.charset.StandardCharsets"
+ }
}
],
"program_flags": [
@@ -346,6 +353,10 @@
},
{
"api_level_below_or_equal": 25,
+ "rewrite_prefix": {
+ "sun.nio.cs.": "j$.sun.nio.cs.",
+ "sun.util.PreHashedMap": "j$.sun.util.PreHashedMap"
+ },
"retarget_method": {
"boolean java.util.Arrays#deepEquals0(java.lang.Object, java.lang.Object)": "java.util.DesugarArrays"
},
@@ -400,6 +411,14 @@
"j$.util.stream.Stream": "java.util.stream.Stream"
}
}
+ },
+ {
+ "api_level_below_or_equal": 18,
+ "retarget_static_field": {
+ "sun.nio.cs.US_ASCII sun.nio.cs.US_ASCII#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#US_ASCII",
+ "sun.nio.cs.ISO_8859_1 sun.nio.cs.ISO_8859_1#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#ISO_8859_1",
+ "sun.nio.cs.UTF_8 sun.nio.cs.UTF_8#INSTANCE": "java.nio.charset.Charset java.nio.charset.StandardCharsets#UTF_8"
+ }
}
],
"shrinker_config": [
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 10d9755..b8489e1 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -548,10 +548,7 @@
appView.setGraphLens(new AppliedGraphLens(appView));
timing.end();
- if (!options.shouldRerunEnqueuer()) {
- // TODO(b/225838009): Support tracing and building LIR in Enqueuer.
- PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
- } else {
+ if (options.shouldRerunEnqueuer()) {
timing.begin("Post optimization code stripping");
try {
GraphConsumer keptGraphConsumer = null;
@@ -656,9 +653,6 @@
timing.end();
}
- // TODO(b/225838009): Support LIR in proto shrinking.
- PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
-
if (appView.options().protoShrinking().isProtoShrinkingEnabled()) {
if (appView.options().protoShrinking().isEnumLiteProtoShrinkingEnabled()) {
appView.protoShrinker().enumLiteProtoShrinker.verifyDeadEnumLiteMapsAreDead();
@@ -692,6 +686,9 @@
}
}
+ // TODO(b/225838009): Check support LIR in bridge remover.
+ PrimaryR8IRConverter.finalizeLirToOutputFormat(appView, timing, executorService);
+
// Insert a member rebinding oracle in the graph to ensure that all subsequent rewritings of
// the application has an applied oracle for looking up non-rebound references.
MemberRebindingIdentityLens memberRebindingIdentityLens =
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 3b68dc5..399d2b7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -35,6 +35,7 @@
public static final int NO_PC_INFO = -1;
private static final int NO_LINE_INFO = -1;
+ private final AppView<?> appView;
private final DexEncodedMethod method;
private final DexItemFactory factory;
private final InternalOptions options;
@@ -63,14 +64,15 @@
// Initial known line for the method.
private int startLine = NO_LINE_INFO;
- public DexDebugEventBuilder(IRCode code, InternalOptions options) {
+ public DexDebugEventBuilder(AppView<?> appView, IRCode code) {
+ this.appView = appView;
this.method = code.method();
- this.factory = options.itemFactory;
- this.options = options;
+ this.factory = appView.dexItemFactory();
+ this.options = appView.options();
}
/** Add events at pc for instruction. */
- public void add(int pc, int postPc, Instruction instruction) {
+ public void add(int pc, int postPc, Instruction instruction, ProgramMethod context) {
boolean isBlockEntry = instruction.getBlock().entry() == instruction;
boolean isBlockExit = instruction.getBlock().exit() == instruction;
@@ -92,7 +94,7 @@
updateLocals(instruction.asDebugLocalsChange());
} else if (pcAdvancing) {
if (!position.isNone() && !position.equals(emittedPosition)) {
- if (options.debug || instruction.instructionInstanceCanThrow()) {
+ if (options.debug || instruction.instructionInstanceCanThrow(appView, context)) {
emitDebugPosition(pc, position);
}
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 854ba5b..4b5815c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -588,6 +588,11 @@
public final DexType kotlinMetadataType = createStaticallyKnownType(kotlinMetadataDescriptor);
public final DexType kotlinJvmNameType = createStaticallyKnownType(kotlinJvmNameDescriptor);
+ public final DexType kotlinEnumEntriesList =
+ createStaticallyKnownType("Lkotlin/enums/EnumEntriesList;");
+ public final DexMethod kotlinEnumEntriesListInit =
+ createInstanceInitializer(kotlinEnumEntriesList, createArrayType(1, enumType));
+
public final DexType javaIoFileType = createStaticallyKnownType("Ljava/io/File;");
public final DexType javaMathBigIntegerType = createStaticallyKnownType("Ljava/math/BigInteger;");
public final DexType javaNioByteOrderType = createStaticallyKnownType("Ljava/nio/ByteOrder;");
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 0dc1d16..b2bc5ce 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -1253,7 +1253,7 @@
TypeElement type = TypeElement.stringClassType(appView, definitelyNotNull());
Value outValue = code.createValue(type, local);
ConstString instruction = new ConstString(outValue, value);
- if (!instruction.instructionInstanceCanThrow()) {
+ if (!instruction.instructionInstanceCanThrow(appView, code.context())) {
return instruction;
}
return null;
@@ -1346,7 +1346,7 @@
DexItemBasedConstString instruction =
new DexItemBasedConstString(outValue, value, nameComputationInfo);
// DexItemBasedConstString cannot throw.
- assert !instruction.instructionInstanceCanThrow();
+ assert !instruction.instructionInstanceCanThrow(appView, code.context());
return instruction;
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index 1c46b14..de4cf6a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -20,11 +20,10 @@
import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
import com.android.tools.r8.ir.optimize.AffectedValues;
import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.WorkList;
import java.util.ArrayList;
import java.util.BitSet;
-import java.util.Deque;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -59,13 +58,9 @@
private final IRCode code;
private final Map<Value, LatticeElement> mapping = new HashMap<>();
- // TODO(b/270398965): Replace LinkedList.
- @SuppressWarnings("JdkObsolete")
- private final Deque<Value> ssaEdges = new LinkedList<>();
+ private final WorkList<Value> ssaEdges = WorkList.newIdentityWorkList();
- // TODO(b/270398965): Replace LinkedList.
- @SuppressWarnings("JdkObsolete")
- private final Deque<BasicBlock> flowEdges = new LinkedList<>();
+ private final WorkList<BasicBlock> flowEdges = WorkList.newIdentityWorkList();
private final BitSet[] executableFlowEdges;
private final BitSet visitedBlocks;
@@ -81,9 +76,9 @@
BasicBlock firstBlock = code.entryBlock();
visitInstructions(firstBlock);
- while (!flowEdges.isEmpty() || !ssaEdges.isEmpty()) {
- while (!flowEdges.isEmpty()) {
- BasicBlock block = flowEdges.poll();
+ while (flowEdges.hasNext() || ssaEdges.hasNext()) {
+ while (flowEdges.hasNext()) {
+ BasicBlock block = flowEdges.removeSeen();
for (Phi phi : block.getPhis()) {
visitPhi(phi);
}
@@ -91,8 +86,8 @@
visitInstructions(block);
}
}
- while (!ssaEdges.isEmpty()) {
- Value value = ssaEdges.poll();
+ while (ssaEdges.hasNext()) {
+ Value value = ssaEdges.removeSeen();
for (Phi phi : value.uniquePhiUsers()) {
visitPhi(phi);
}
@@ -186,7 +181,7 @@
if (!element.isTop()) {
LatticeElement currentPhiElement = getLatticeElement(phi);
if (currentPhiElement.meet(element) != currentPhiElement) {
- ssaEdges.add(phi);
+ ssaEdges.addIfNotSeen(phi);
setLatticeElement(phi, element);
}
}
@@ -200,12 +195,12 @@
}
private void visitInstruction(Instruction instruction) {
- if (instruction.outValue() != null && !instruction.isDebugLocalUninitialized()) {
+ if (instruction.hasOutValue() && !instruction.isDebugLocalUninitialized()) {
LatticeElement element = instruction.evaluate(code, this::getLatticeElement);
LatticeElement currentLattice = getLatticeElement(instruction.outValue());
if (currentLattice.meet(element) != currentLattice) {
setLatticeElement(instruction.outValue(), element);
- ssaEdges.add(instruction.outValue());
+ ssaEdges.addIfNotSeen(instruction.outValue());
}
}
if (instruction.isJumpInstruction()) {
@@ -224,7 +219,7 @@
BasicBlock target = theIf.targetFromCondition(element.asConst().getConstNumber());
if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
setExecutableEdge(jumpInstBlockNumber, target.getNumber());
- flowEdges.add(target);
+ flowEdges.addIfNotSeen(target);
}
return;
}
@@ -237,7 +232,7 @@
BasicBlock target = theIf.targetFromCondition(leftNumber, rightNumber);
if (!isExecutableEdge(jumpInstBlockNumber, target.getNumber())) {
setExecutableEdge(jumpInstBlockNumber, target.getNumber());
- flowEdges.add(target);
+ flowEdges.addIfNotSeen(target);
}
return;
}
@@ -255,7 +250,7 @@
}
assert target != null;
setExecutableEdge(jumpInstBlockNumber, target.getNumber());
- flowEdges.add(target);
+ flowEdges.addIfNotSeen(target);
return;
}
} else if (jumpInstruction.isStringSwitch()) {
@@ -266,7 +261,7 @@
assert switchElement.asConst().getConstNumber().isZero();
BasicBlock target = switchInst.fallthroughBlock();
setExecutableEdge(jumpInstBlockNumber, target.getNumber());
- flowEdges.add(target);
+ flowEdges.addIfNotSeen(target);
return;
}
} else {
@@ -276,7 +271,7 @@
for (BasicBlock dst : jumpInstBlock.getSuccessors()) {
if (!isExecutableEdge(jumpInstBlockNumber, dst.getNumber())) {
setExecutableEdge(jumpInstBlockNumber, dst.getNumber());
- flowEdges.add(dst);
+ flowEdges.addIfNotSeen(dst);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
index d274600..187ac76 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -223,7 +223,7 @@
private boolean definesValueWithNonLocalUsages(Instruction instruction) {
if (instruction.hasOutValue()) {
Value outValue = instruction.outValue();
- if (outValue.numberOfPhiUsers() > 0) {
+ if (outValue.hasPhiUsers()) {
return true;
}
for (Instruction user : outValue.uniqueUsers()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 5907290..5fbfb59 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -156,6 +156,7 @@
}
IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(code, instruction);
}
+ code.removeRedundantBlocks();
assert code.isConsistentSSA(appView);
}
@@ -181,7 +182,7 @@
OptimizationFeedbackIgnore.getInstance(),
methodProcessor,
methodProcessingContext,
- MethodConversionOptions.forPostLirPhase(appView)),
+ MethodConversionOptions.forLirPhase(appView)),
executorService);
timing.end();
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index 49b2e91..7caae94 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -257,7 +257,7 @@
OptimizationFeedbackIgnore.getInstance(),
methodProcessor,
methodProcessingContext,
- MethodConversionOptions.forPostLirPhase(appView)),
+ MethodConversionOptions.forLirPhase(appView)),
executorService);
timing.end();
}
@@ -311,6 +311,7 @@
assert false;
}
}
+ code.removeRedundantBlocks();
assert code.isConsistentSSA(appView);
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
index bc0aef8..ef1de40 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
@@ -92,7 +92,7 @@
debugLocalInfo);
DexItemBasedConstString instruction =
new DexItemBasedConstString(returnedValue, item, nameComputationInfo);
- assert !instruction.instructionInstanceCanThrow();
+ assert !instruction.instructionInstanceCanThrow(appView, context);
return instruction;
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
index d51ff72..78e547c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleStringValue.java
@@ -79,7 +79,7 @@
stringClassType(appView, definitelyNotNull()),
debugLocalInfo);
ConstString instruction = new ConstString(returnedValue, string);
- assert !instruction.instructionInstanceCanThrow();
+ assert !instruction.instructionInstanceCanThrow(appView, context);
return instruction;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
index eb20267..64305dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayAccess.java
@@ -4,6 +4,8 @@
package com.android.tools.r8.ir.code;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
import java.util.List;
public abstract class ArrayAccess extends Instruction implements ImpreciseMemberTypeInstruction {
@@ -48,7 +50,7 @@
}
@Override
- public boolean instructionInstanceCanThrow() {
+ public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
// TODO(b/203731608): Add parameters to the method and use abstract value in R8.
if (index().isConstant() && !array().isPhi() && array().definition.isNewArrayEmpty()) {
Value newArraySizeValue = array().definition.asNewArrayEmpty().size();
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
index ab357ed..836a5a5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayGet.java
@@ -290,11 +290,6 @@
@Override
public void buildLir(LirBuilder<Value, ?> builder) {
- if (getMemberType().isObject()) {
- DexType destType = dest().getType().asReferenceType().toDexType(builder.factory());
- builder.addArrayGetObject(destType, array(), index());
- } else {
- builder.addArrayGetPrimitive(getMemberType(), array(), index());
- }
+ builder.addArrayGet(getMemberType(), array(), index());
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
index 483e19d..5daba45 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
@@ -139,15 +139,9 @@
@Override
public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
- // In debug mode, ArrayPut has a side-effect on the locals.
- if (appView.options().debug) {
- return true;
- }
-
- // In release mode, ArrayPut has side-effects on the input array, unless the index is in bounds
- // and the array is never used.
+ // Check that the array is guaranteed to be non-null and that the index is within bounds.
Value array = array().getAliasedValue();
- if (array.isPhi() || !array.definition.isNewArrayEmpty()) {
+ if (!array.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)) {
return true;
}
@@ -168,6 +162,10 @@
return true;
}
+ if (array.hasLocalInfo() || indexValue.hasLocalInfo() || sizeValue.hasLocalInfo()) {
+ return true;
+ }
+
// Check for type errors.
TypeElement arrayType = array.getType();
TypeElement valueType = value().getType();
@@ -175,10 +173,7 @@
return true;
}
TypeElement memberType = arrayType.asArrayType().getMemberTypeAsValueType();
- if (!valueType.lessThanOrEqualUpToNullability(memberType, appView)) {
- return true;
- }
- return false;
+ return !valueType.lessThanOrEqualUpToNullability(memberType, appView);
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
index 86c95b3..d9f1db8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstClass.java
@@ -118,11 +118,6 @@
}
@Override
- public boolean instructionInstanceCanThrow() {
- return true;
- }
-
- @Override
public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
DexType baseType = getValue().toBaseType(appView.dexItemFactory());
if (baseType.isPrimitiveType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index 9c1a421..5106a8b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -119,7 +119,6 @@
return this;
}
- @Override
public boolean instructionInstanceCanThrow() {
// The const-string instruction can be a throwing instruction in DEX, if decode() fails.
try {
@@ -135,9 +134,15 @@
}
@Override
+ public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
+ return instructionInstanceCanThrow();
+ }
+
+ @Override
public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
// No side-effect, such as throwing an exception, in CF.
- if (appView.options().isGeneratingClassFiles() || !instructionInstanceCanThrow()) {
+ if (appView.options().isGeneratingClassFiles()
+ || !instructionInstanceCanThrow(appView, code.context())) {
return DeadInstructionResult.deadIfOutValueIsDead();
}
return DeadInstructionResult.notDead();
@@ -171,7 +176,7 @@
@Override
public AbstractValue getAbstractValue(
AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
- if (!instructionInstanceCanThrow()) {
+ if (!instructionInstanceCanThrow(appView, context)) {
return appView.abstractValueFactory().createSingleStringValue(value);
}
return UnknownValue.getInstance();
diff --git a/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java b/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
index 51182ac..9b4795e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DexItemBasedConstString.java
@@ -128,7 +128,7 @@
}
@Override
- public boolean instructionInstanceCanThrow() {
+ public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
// A const-string instruction can usually throw an exception if the decoding of the string
// fails. Since this string corresponds to a type or member name, though, decoding cannot fail.
return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 468c7bf..8808381 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -120,6 +120,10 @@
return result;
}
+ public boolean mayHaveIf() {
+ return get(Opcodes.IF);
+ }
+
public boolean mayHaveInitClass() {
return get(Opcodes.INIT_CLASS);
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 2da97aa..dee8f72 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -604,17 +604,13 @@
return false;
}
- public boolean instructionInstanceCanThrow() {
- return instructionTypeCanThrow();
- }
-
public boolean instructionMayHaveSideEffects(AppView<?> appView, ProgramMethod context) {
return instructionMayHaveSideEffects(appView, context, SideEffectAssumption.NONE);
}
public boolean instructionMayHaveSideEffects(
AppView<?> appView, ProgramMethod context, SideEffectAssumption assumption) {
- return instructionInstanceCanThrow();
+ return instructionInstanceCanThrow(appView, context);
}
/**
@@ -625,7 +621,7 @@
AppView<?> appView, ProgramMethod context);
public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
- return instructionInstanceCanThrow();
+ return instructionTypeCanThrow();
}
/** Returns true is this instruction can be treated as dead code if its outputs are not used. */
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
index 477bd14..1d63180 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayEmpty.java
@@ -78,7 +78,7 @@
}
@Override
- public boolean instructionInstanceCanThrow() {
+ public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
return !(size().definition != null
&& size().definition.isConstNumber()
&& size().definition.asConstNumber().getRawValue() >= 0
@@ -89,7 +89,7 @@
public AbstractValue getAbstractValue(
AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
if (!instructionMayHaveSideEffects(appView, context) && size().getType().isInt()) {
- assert !instructionInstanceCanThrow();
+ assert !instructionInstanceCanThrow(appView, context);
return StatefulObjectValue.create(
appView
.abstractValueFactory()
@@ -100,7 +100,7 @@
@Override
public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
- if (instructionInstanceCanThrow()) {
+ if (instructionInstanceCanThrow(appView, code.context())) {
return DeadInstructionResult.notDead();
}
// This would belong better in instructionInstanceCanThrow, but that is not passed an appInfo.
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
index ddf7cba..226bf4f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
@@ -133,7 +133,7 @@
public AbstractValue getAbstractValue(
AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
if (!instructionMayHaveSideEffects(appView, context) && size <= Integer.MAX_VALUE) {
- assert !instructionInstanceCanThrow();
+ assert !instructionInstanceCanThrow(appView, context);
return StatefulObjectValue.create(
appView.abstractValueFactory().createKnownLengthArrayState((int) size));
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 1ddfeac..14b9549 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -247,7 +247,7 @@
} while (!ifsNeedingRewrite.isEmpty());
// Build instructions.
- DexDebugEventBuilder debugEventBuilder = new DexDebugEventBuilder(ir, options);
+ DexDebugEventBuilder debugEventBuilder = new DexDebugEventBuilder(appView, ir);
List<DexInstruction> dexInstructions = new ArrayList<>(numberOfInstructions);
int instructionOffset = 0;
for (com.android.tools.r8.ir.code.Instruction irInstruction : ir.instructions()) {
@@ -260,7 +260,8 @@
dexInstruction.setOffset(instructionOffset);
instructionOffset += dexInstruction.getSize();
}
- debugEventBuilder.add(instructionStartOffset, instructionOffset, irInstruction);
+ debugEventBuilder.add(
+ instructionStartOffset, instructionOffset, irInstruction, getProgramMethod());
}
// Workaround dalvik tracing bug, where the dalvik tracing JIT can end up tracing
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index b8a54fb..7ce1b99 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -123,7 +123,7 @@
}
FilledArrayConversionInfo info =
computeConversionInfo(
- candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex()));
+ code, candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex()));
if (info == null) {
continue;
}
@@ -252,7 +252,7 @@
}
private FilledArrayConversionInfo computeConversionInfo(
- FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) {
+ IRCode code, FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) {
NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty;
assert it.peekPrevious() == newArrayEmpty;
Value arrayValue = newArrayEmpty.outValue();
@@ -279,7 +279,8 @@
// optimization so that we do not transform half-initialized arrays into fully initialized
// arrays on exceptional edges. If the block has no handlers it is not observable so
// we perform the rewriting.
- if (block.hasCatchHandlers() && instruction.instructionInstanceCanThrow()) {
+ if (block.hasCatchHandlers()
+ && instruction.instructionInstanceCanThrow(appView, code.context())) {
return null;
}
if (!users.contains(instruction)) {
@@ -299,8 +300,7 @@
if (!arrayPut.index().isConstNumber()) {
return null;
}
- if (arrayPut.instructionInstanceCanThrow()) {
- assert false;
+ if (arrayPut.instructionInstanceCanThrow(appView, code.context())) {
return null;
}
int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index bc9c351..c9df777 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -75,7 +75,7 @@
@Override
protected boolean shouldRewriteCode(IRCode code) {
- return true;
+ return code.metadata().mayHaveIf() || code.metadata().mayHaveSwitch();
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 3bb77d8..5d078fe 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -61,7 +61,9 @@
@Override
protected boolean shouldRewriteCode(IRCode code) {
// This is relevant only if a loop may be present, which implies at least 4 blocks.
- return appView.options().enableLoopUnrolling && code.getBlocks().size() >= 4;
+ return appView.options().enableLoopUnrolling
+ && code.metadata().mayHaveIf()
+ && code.getBlocks().size() >= 4;
}
private boolean isComparisonBlock(BasicBlock comparisonBlockCandidate) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
index 1e1b91c..566996d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SplitBranch.java
@@ -40,7 +40,7 @@
@Override
protected boolean shouldRewriteCode(IRCode code) {
// This is relevant only if there is a diamond followed by an if which is a minimum of 6 blocks.
- return code.getBlocks().size() >= 6;
+ return code.metadata().mayHaveIf() && code.getBlocks().size() >= 6;
}
/**
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 7e03d91..bf7ebd5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -21,6 +21,7 @@
import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicClass;
import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicDesugaringEventConsumer;
import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryAPIConverterEventConsumer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibRewriterEventConsumer;
import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryRetargeterSynthesizerEventConsumer.DesugaredLibraryRetargeterInstructionEventConsumer;
import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialBridgeInfo;
import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaringEventConsumer;
@@ -66,7 +67,8 @@
DesugaredLibraryAPIConverterEventConsumer,
ClasspathEmulatedInterfaceSynthesizerEventConsumer,
ApiInvokeOutlinerDesugaringEventConsumer,
- VarHandleDesugaringEventConsumer {
+ VarHandleDesugaringEventConsumer,
+ DesugaredLibRewriterEventConsumer {
public static CfInstructionDesugaringEventConsumer createForD8(
AppView<?> appView,
@@ -187,6 +189,11 @@
}
@Override
+ public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+ methodProcessor.scheduleMethodForProcessing(method, outermostEventConsumer);
+ }
+
+ @Override
public void acceptRecordEqualsHelperMethod(ProgramMethod method, ProgramMethod context) {
// Intentionally empty. Added to the program using ProgramAdditions.
}
@@ -527,6 +534,11 @@
}
@Override
+ public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+ // Intentionally empty. The method will be hit by tracing if required.
+ }
+
+ @Override
public void acceptRecordEqualsHelperMethod(ProgramMethod method, ProgramMethod context) {
// Intentionally empty. The method will be hit by tracing if required.
}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index 184dff0..f40acbb 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.ir.desugar.constantdynamic.ConstantDynamicInstructionDesugaring;
import com.android.tools.r8.ir.desugar.desugaredlibrary.apiconversion.DesugaredLibraryAPIConverter;
import com.android.tools.r8.ir.desugar.desugaredlibrary.disabledesugarer.DesugaredLibraryDisableDesugarer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryLibRewriter;
import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryRetargeter;
import com.android.tools.r8.ir.desugar.icce.AlwaysThrowingInstructionDesugaring;
import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
@@ -84,6 +85,10 @@
}
this.nestBasedAccessDesugaring = NestBasedAccessDesugaring.create(appView);
BackportedMethodRewriter backportedMethodRewriter = new BackportedMethodRewriter(appView);
+ DesugaredLibraryLibRewriter desugaredLibRewriter = DesugaredLibraryLibRewriter.create(appView);
+ if (desugaredLibRewriter != null) {
+ desugarings.add(desugaredLibRewriter);
+ }
desugaredLibraryRetargeter =
appView.options().machineDesugaredLibrarySpecification.hasRetargeting()
? new DesugaredLibraryRetargeter(appView)
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java
new file mode 100644
index 0000000..78484e3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibRewriterEventConsumer.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface DesugaredLibRewriterEventConsumer {
+
+ void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java
new file mode 100644
index 0000000..0fdac18
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods.java
@@ -0,0 +1,232 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ***********************************************************************************
+// GENERATED FILE. DO NOT EDIT! See GenerateDesugaredLibraryBridge.java.
+// ***********************************************************************************
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfIf;
+import com.android.tools.r8.cf.code.CfInstanceOf;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.cf.code.frame.FrameType;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.IfType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+public final class DesugaredLibraryCfMethods {
+
+ public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+ factory.createSynthesizedType("Landroidx/navigation/NavType$Companion;");
+ factory.createSynthesizedType("Landroidx/navigation/NavType;");
+ factory.createSynthesizedType("Ljava/lang/ClassNotFoundException;");
+ factory.createSynthesizedType("Ljava/lang/RuntimeException;");
+ }
+
+ public static CfCode DesugaredLibraryBridge_fromArgType(
+ DexItemFactory factory, DexMethod method) {
+ CfLabel label0 = new CfLabel();
+ CfLabel label1 = new CfLabel();
+ CfLabel label2 = new CfLabel();
+ CfLabel label3 = new CfLabel();
+ CfLabel label4 = new CfLabel();
+ CfLabel label5 = new CfLabel();
+ CfLabel label6 = new CfLabel();
+ CfLabel label7 = new CfLabel();
+ CfLabel label8 = new CfLabel();
+ return new CfCode(
+ method.holder,
+ 4,
+ 4,
+ ImmutableList.of(
+ label0,
+ new CfLoad(ValueType.OBJECT, 1),
+ new CfIf(IfType.EQ, ValueType.OBJECT, label1),
+ new CfLoad(ValueType.OBJECT, 1),
+ new CfConstString(factory.createString("java")),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringType,
+ factory.createProto(factory.booleanType, factory.stringType),
+ factory.createString("startsWith")),
+ false),
+ new CfIf(IfType.NE, ValueType.INT, label2),
+ label1,
+ new CfFrame(
+ new Int2ObjectAVLTreeMap<>(
+ new int[] {0, 1, 2},
+ new FrameType[] {
+ FrameType.initializedNonNullReference(
+ factory.createType("Landroidx/navigation/NavType$Companion;")),
+ FrameType.initializedNonNullReference(factory.stringType),
+ FrameType.initializedNonNullReference(factory.stringType)
+ })),
+ new CfLoad(ValueType.OBJECT, 0),
+ new CfLoad(ValueType.OBJECT, 1),
+ new CfLoad(ValueType.OBJECT, 2),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.createType("Landroidx/navigation/NavType$Companion;"),
+ factory.createProto(
+ factory.createType("Landroidx/navigation/NavType;"),
+ factory.stringType,
+ factory.stringType),
+ factory.createString("fromArgType")),
+ false),
+ new CfReturn(ValueType.OBJECT),
+ label2,
+ new CfFrame(
+ new Int2ObjectAVLTreeMap<>(
+ new int[] {0, 1, 2},
+ new FrameType[] {
+ FrameType.initializedNonNullReference(
+ factory.createType("Landroidx/navigation/NavType$Companion;")),
+ FrameType.initializedNonNullReference(factory.stringType),
+ FrameType.initializedNonNullReference(factory.stringType)
+ })),
+ new CfLoad(ValueType.OBJECT, 0),
+ new CfNew(factory.stringBuilderType),
+ new CfStackInstruction(CfStackInstruction.Opcode.Dup),
+ new CfInvoke(
+ 183,
+ factory.createMethod(
+ factory.stringBuilderType,
+ factory.createProto(factory.voidType),
+ factory.createString("<init>")),
+ false),
+ new CfConstString(factory.createString("j$")),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringBuilderType,
+ factory.createProto(factory.stringBuilderType, factory.stringType),
+ factory.createString("append")),
+ false),
+ new CfLoad(ValueType.OBJECT, 1),
+ new CfConstString(factory.createString("java")),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringType,
+ factory.createProto(factory.intType),
+ factory.createString("length")),
+ false),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringType,
+ factory.createProto(factory.stringType, factory.intType),
+ factory.createString("substring")),
+ false),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringBuilderType,
+ factory.createProto(factory.stringBuilderType, factory.stringType),
+ factory.createString("append")),
+ false),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.stringBuilderType,
+ factory.createProto(factory.stringType),
+ factory.createString("toString")),
+ false),
+ new CfLoad(ValueType.OBJECT, 2),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.createType("Landroidx/navigation/NavType$Companion;"),
+ factory.createProto(
+ factory.createType("Landroidx/navigation/NavType;"),
+ factory.stringType,
+ factory.stringType),
+ factory.createString("fromArgType")),
+ false),
+ label3,
+ new CfReturn(ValueType.OBJECT),
+ label4,
+ new CfFrame(
+ new Int2ObjectAVLTreeMap<>(
+ new int[] {0, 1, 2},
+ new FrameType[] {
+ FrameType.initializedNonNullReference(
+ factory.createType("Landroidx/navigation/NavType$Companion;")),
+ FrameType.initializedNonNullReference(factory.stringType),
+ FrameType.initializedNonNullReference(factory.stringType)
+ }),
+ new ArrayDeque<>(
+ Arrays.asList(
+ FrameType.initializedNonNullReference(
+ factory.createType("Ljava/lang/RuntimeException;"))))),
+ new CfStore(ValueType.OBJECT, 3),
+ label5,
+ new CfLoad(ValueType.OBJECT, 3),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.createType("Ljava/lang/RuntimeException;"),
+ factory.createProto(factory.throwableType),
+ factory.createString("getCause")),
+ false),
+ new CfInstanceOf(factory.createType("Ljava/lang/ClassNotFoundException;")),
+ new CfIf(IfType.EQ, ValueType.INT, label7),
+ label6,
+ new CfLoad(ValueType.OBJECT, 0),
+ new CfLoad(ValueType.OBJECT, 1),
+ new CfLoad(ValueType.OBJECT, 2),
+ new CfInvoke(
+ 182,
+ factory.createMethod(
+ factory.createType("Landroidx/navigation/NavType$Companion;"),
+ factory.createProto(
+ factory.createType("Landroidx/navigation/NavType;"),
+ factory.stringType,
+ factory.stringType),
+ factory.createString("fromArgType")),
+ false),
+ new CfReturn(ValueType.OBJECT),
+ label7,
+ new CfFrame(
+ new Int2ObjectAVLTreeMap<>(
+ new int[] {0, 1, 2, 3},
+ new FrameType[] {
+ FrameType.initializedNonNullReference(
+ factory.createType("Landroidx/navigation/NavType$Companion;")),
+ FrameType.initializedNonNullReference(factory.stringType),
+ FrameType.initializedNonNullReference(factory.stringType),
+ FrameType.initializedNonNullReference(
+ factory.createType("Ljava/lang/RuntimeException;"))
+ })),
+ new CfLoad(ValueType.OBJECT, 3),
+ new CfThrow(),
+ label8),
+ ImmutableList.of(
+ new CfTryCatch(
+ label2,
+ label3,
+ ImmutableList.of(factory.createType("Ljava/lang/RuntimeException;")),
+ ImmutableList.of(label4))),
+ ImmutableList.of());
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java
new file mode 100644
index 0000000..2c039d1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryLibRewriter.java
@@ -0,0 +1,146 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.DesugarDescription;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.BiFunction;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * This holds specific rewritings when using desugared library and specific libraries such as
+ * androidx.
+ */
+public class DesugaredLibraryLibRewriter implements CfInstructionDesugaring {
+
+ private final AppView<?> appView;
+ private final Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings;
+
+ private DesugaredLibraryLibRewriter(
+ AppView<?> appView,
+ Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings) {
+ this.appView = appView;
+ this.rewritings = rewritings;
+ }
+
+ public static DesugaredLibraryLibRewriter create(AppView<?> appView) {
+ if (appView.options().machineDesugaredLibrarySpecification.getRewriteType().isEmpty()) {
+ return null;
+ }
+ Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> rewritings = computeMap(appView);
+ if (rewritings.isEmpty()) {
+ return null;
+ }
+ return new DesugaredLibraryLibRewriter(appView, rewritings);
+ }
+
+ public static Map<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> computeMap(
+ AppView<?> appView) {
+ DexItemFactory factory = appView.dexItemFactory();
+ DexType navType = factory.createType("Landroidx/navigation/NavType;");
+ if (!appView.appInfo().hasDefinitionForWithoutExistenceAssert(navType)) {
+ return ImmutableMap.of();
+ }
+ ImmutableMap.Builder<DexMethod, BiFunction<DexItemFactory, DexMethod, CfCode>> builder =
+ ImmutableMap.builder();
+ DexType navTypeCompanion = factory.createType("Landroidx/navigation/NavType$Companion;");
+ DexProto fromProto = factory.createProto(navType, factory.stringType, factory.stringType);
+ DexString name = factory.createString("fromArgType");
+ DexMethod from = factory.createMethod(navTypeCompanion, fromProto, name);
+ DexClassAndMethod dexClassAndMethod = appView.definitionFor(from);
+ if (dexClassAndMethod == null) {
+ appView
+ .options()
+ .reporter
+ .warning(
+ "The class "
+ + navType
+ + " is present but not the method "
+ + from
+ + " which suggests some unsupported set-up where androidx is pre-shrunk without"
+ + " keeping the method "
+ + from
+ + ".");
+ return ImmutableMap.of();
+ }
+ BiFunction<DexItemFactory, DexMethod, CfCode> cfCodeProvider =
+ DesugaredLibraryCfMethods::DesugaredLibraryBridge_fromArgType;
+ builder.put(from, cfCodeProvider);
+ return builder.build();
+ }
+
+ @Override
+ public DesugarDescription compute(CfInstruction instruction, ProgramMethod context) {
+ if (appView
+ .getSyntheticItems()
+ .isSyntheticOfKind(context.getHolderType(), kinds -> kinds.DESUGARED_LIBRARY_BRIDGE)) {
+ return DesugarDescription.nothing();
+ }
+ if (instruction.isInvoke() && rewritings.containsKey(instruction.asInvoke().getMethod())) {
+ return DesugarDescription.builder()
+ .setDesugarRewrite(
+ (freshLocalProvider,
+ localStackAllocator,
+ eventConsumer,
+ localContext,
+ methodProcessingContext,
+ desugarings,
+ dexItemFactory) -> {
+ DexMethod newInvokeTarget =
+ ensureBridge(
+ instruction.asInvoke().getMethod(),
+ eventConsumer,
+ methodProcessingContext,
+ localContext);
+ assert appView.definitionFor(newInvokeTarget.getHolderType()) != null;
+ assert !appView.definitionFor(newInvokeTarget.getHolderType()).isInterface();
+ return Collections.singletonList(
+ new CfInvoke(Opcodes.INVOKESTATIC, newInvokeTarget, false));
+ })
+ .build();
+ }
+ return DesugarDescription.nothing();
+ }
+
+ private DexMethod ensureBridge(
+ DexMethod source,
+ CfInstructionDesugaringEventConsumer eventConsumer,
+ MethodProcessingContext methodProcessingContext,
+ ProgramMethod localContext) {
+ BiFunction<DexItemFactory, DexMethod, CfCode> target = rewritings.get(source);
+ ProgramMethod newMethod =
+ appView
+ .getSyntheticItems()
+ .createMethod(
+ kinds -> kinds.DESUGARED_LIBRARY_BRIDGE,
+ methodProcessingContext.createUniqueContext(),
+ appView,
+ builder ->
+ builder
+ .disableAndroidApiLevelCheck()
+ .setProto(appView.dexItemFactory().prependHolderToProto(source))
+ .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+ .setCode(methodSig -> target.apply(appView.dexItemFactory(), methodSig)));
+ eventConsumer.acceptDesugaredLibraryBridge(newMethod, localContext);
+ return newMethod.getReference();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
index bd8b0fc..394241a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordFieldValuesRewriter.java
@@ -72,7 +72,7 @@
programMethod,
appView,
programMethod.getOrigin(),
- MethodConversionOptions.forPostLirPhase(appView));
+ MethodConversionOptions.forLirPhase(appView));
boolean done = false;
ListIterator<BasicBlock> blockIterator = irCode.listIterator();
while (blockIterator.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 892f856..f658311 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -646,7 +646,8 @@
} else if (instr.instructionMayHaveSideEffects(appView, code.context())) {
// If the current instruction is const-string, this could load the parameter name.
// Just make sure it is indeed not throwing.
- if (instr.isConstString() && !instr.instructionInstanceCanThrow()) {
+ if (instr.isConstString()
+ && !instr.instructionInstanceCanThrow(appView, code.context())) {
return InstructionEffect.NO_EFFECT;
}
// We found a side effect before a NPE check.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
index a4e5008..77121b6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
@@ -79,7 +79,7 @@
Instruction definition = argument.definition;
if (definition.isConstString()) {
ConstString constString = definition.asConstString();
- if (!constString.instructionInstanceCanThrow()) {
+ if (!constString.instructionInstanceCanThrow(appView, code.context())) {
String value = StringUtils.toLowerCase(constString.getValue().toString());
if (value.equals("true")) {
instructionIterator.replaceCurrentInstructionWithConstInt(code, 1);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
index bc6f506..a579344 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -94,6 +94,8 @@
.add(dexItemFactory.stringMembers.trim)
.addAll(dexItemFactory.classMethods.getNames)
.addAll(dexItemFactory.boxedValueOfMethods())
+ // Required to unbox recent Kotlin enums (See b/268005228).
+ .add(dexItemFactory.kotlinEnumEntriesListInit)
.build();
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index eefac9a..55bef08 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -1259,7 +1259,8 @@
continue;
}
int currentPositionIndex = outlinePositionIndex++;
- if (current.getPosition() != null && current.instructionInstanceCanThrow()) {
+ if (current.getPosition() != null
+ && current.instructionInstanceCanThrow(appView, method)) {
positionBuilder.addOutlinePosition(currentPositionIndex, current.getPosition());
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 202dc62..fb50e54 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -188,7 +188,7 @@
Value rcv = invoke.getReceiver().getAliasedValue();
if (rcv.definition == null
|| !rcv.definition.isConstString()
- || rcv.definition.asConstString().instructionInstanceCanThrow()
+ || rcv.definition.asConstString().instructionInstanceCanThrow(appView, code.context())
|| rcv.hasLocalInfo()) {
continue;
}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
index 04392f3..583bd40 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
@@ -89,17 +89,21 @@
GraphLens kotlinMetadataLens = appView.getKotlinMetadataLens();
DexType rewrittenMetadataType =
graphLens.lookupClassType(factory.kotlinMetadataType, kotlinMetadataLens);
- DexClass kotlinMetadata = appView.definitionFor(rewrittenMetadataType);
+ // The Kotlin metadata may be present in the input but pruned away in the final tree shaking.
+ DexClass kotlinMetadata =
+ appView.appInfo().definitionForWithoutExistenceAssert(rewrittenMetadataType);
WriteMetadataFieldInfo writeMetadataFieldInfo =
- new WriteMetadataFieldInfo(
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.kind),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.metadataVersion),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.bytecodeVersion),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data1),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data2),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraString),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.packageName),
- kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraInt));
+ kotlinMetadata == null
+ ? WriteMetadataFieldInfo.rewriteAll()
+ : new WriteMetadataFieldInfo(
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.kind),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.metadataVersion),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.bytecodeVersion),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data1),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.data2),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraString),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.packageName),
+ kotlinMetadataFieldExists(kotlinMetadata, appView, kotlin.metadata.extraInt));
ThreadUtils.processItems(
appView.appInfo().classes(),
clazz -> {
diff --git a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
index 7f3335f..2e191a7 100644
--- a/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/IR2LirConverter.java
@@ -133,7 +133,7 @@
Value[] operands = new Value[block.getPredecessors().size()];
for (Phi phi : block.getPhis()) {
permuteOperands(phi.getOperands(), permutation, operands);
- builder.addPhi(phi.getType(), Arrays.asList(operands));
+ builder.addPhi(Arrays.asList(operands));
valuesOffset++;
}
}
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 16a09d6..a8cb780 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -927,7 +927,7 @@
}
@Override
- public void onPhi(DexType type, List<EV> operands) {
+ public void onPhi(List<EV> operands) {
// The type of the phi is determined by its operands during type widening.
Phi phi = getPhiForNextInstructionAndAdvanceState(TypeElement.getBottom());
List<Value> values = new ArrayList<>(operands.size());
@@ -1023,19 +1023,18 @@
}
@Override
- public void onArrayGetObject(DexType unusedType, EV array, EV index) {
- // TODO(b/225838009): Remove type and unify object/primitive methods now that it is computed.
- // The output type depends on its input array member type, so it is computed by widening.
- Value dest = getOutValueForNextInstruction(TypeElement.getBottom());
- addInstruction(new ArrayGet(MemberType.OBJECT, dest, getValue(array), getValue(index)));
- }
-
- @Override
- public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
- // Convert the member type to a "stack value type", e.g., byte, char etc to int.
- ValueType valueType = ValueType.fromMemberType(type);
- DexType dexType = valueType.toDexType(appView.dexItemFactory());
- Value dest = getOutValueForNextInstruction(dexType.toTypeElement(appView));
+ public void onArrayGet(MemberType type, EV array, EV index) {
+ TypeElement typeElement;
+ if (type.isObject()) {
+ // The actual object type must be computed from its array value.
+ typeElement = TypeElement.getBottom();
+ } else {
+ // Convert the member type to a "stack value type", e.g., byte, char etc to int.
+ ValueType valueType = ValueType.fromMemberType(type);
+ DexType dexType = valueType.toDexType(appView.dexItemFactory());
+ typeElement = dexType.toTypeElement(appView);
+ }
+ Value dest = getOutValueForNextInstruction(typeElement);
addInstruction(new ArrayGet(type, dest, getValue(array), getValue(index)));
}
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 0cd52a4..b450066 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -773,9 +773,8 @@
return addOneItemInstruction(LirOpcodes.MOVEEXCEPTION, exceptionType);
}
- public LirBuilder<V, EV> addPhi(TypeElement type, List<V> operands) {
- DexType dexType = toDexType(type);
- return addInstructionTemplate(LirOpcodes.PHI, Collections.singletonList(dexType), operands);
+ public LirBuilder<V, EV> addPhi(List<V> operands) {
+ return addInstructionTemplate(LirOpcodes.PHI, Collections.emptyList(), operands);
}
public LirBuilder<V, EV> addDebugLocalWrite(V src) {
@@ -881,14 +880,12 @@
return addOneValueInstruction(opcode, value);
}
- public LirBuilder<V, EV> addArrayGetObject(DexType destType, V array, V index) {
- return addInstructionTemplate(
- LirOpcodes.AALOAD, Collections.singletonList(destType), ImmutableList.of(array, index));
- }
-
- public LirBuilder<V, EV> addArrayGetPrimitive(MemberType memberType, V array, V index) {
+ public LirBuilder<V, EV> addArrayGet(MemberType memberType, V array, V index) {
int opcode;
switch (memberType) {
+ case OBJECT:
+ opcode = LirOpcodes.AALOAD;
+ break;
case BOOLEAN_OR_BYTE:
opcode = LirOpcodes.BALOAD;
break;
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index e3ca61d..5f1411d 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -113,23 +113,12 @@
}
private void onArrayGetInternal(MemberType type, LirInstructionView view) {
- if (type.isObject()) {
- DexType destType = (DexType) getConstantItem(view.getNextConstantOperand());
- EV array = getNextValueOperand(view);
- EV index = getNextValueOperand(view);
- onArrayGetObject(destType, array, index);
- } else {
- EV array = getNextValueOperand(view);
- EV index = getNextValueOperand(view);
- onArrayGetPrimitive(type, array, index);
- }
+ EV array = getNextValueOperand(view);
+ EV index = getNextValueOperand(view);
+ onArrayGet(type, array, index);
}
- public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
- onInstruction();
- }
-
- public void onArrayGetObject(DexType type, EV array, EV index) {
+ public void onArrayGet(MemberType type, EV array, EV index) {
onInstruction();
}
@@ -497,7 +486,7 @@
onInstruction();
}
- public void onPhi(DexType type, List<EV> operands) {
+ public void onPhi(List<EV> operands) {
onInstruction();
}
@@ -1176,12 +1165,11 @@
}
case LirOpcodes.PHI:
{
- DexType type = getNextDexTypeOperand(view);
List<EV> operands = new ArrayList<>();
while (view.hasMoreOperands()) {
operands.add(getNextValueOperand(view));
}
- onPhi(type, operands);
+ onPhi(operands);
return;
}
case LirOpcodes.FALLTHROUGH:
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index c3a8406..f2fcbac 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -385,14 +385,7 @@
}
@Override
- public void onArrayGetPrimitive(MemberType type, EV array, EV index) {
- appendOutValue();
- appendValueArguments(array, index);
- builder.append(type);
- }
-
- @Override
- public void onArrayGetObject(DexType type, EV array, EV index) {
+ public void onArrayGet(MemberType type, EV array, EV index) {
appendOutValue();
appendValueArguments(array, index);
builder.append(type);
@@ -410,10 +403,9 @@
}
@Override
- public void onPhi(DexType type, List<EV> operands) {
+ public void onPhi(List<EV> operands) {
appendOutValue();
appendValueArguments(operands);
- builder.append(type);
}
@Override
diff --git a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
index 3b43c56..00e00ca 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
@@ -87,7 +87,7 @@
}
@Override
- public void onPhi(DexType type, List<EV> operands) {
+ public void onPhi(List<EV> operands) {
// Nothing to register.
}
diff --git a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
index 94232e2..3050741 100644
--- a/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/profile/rewriting/ProfileRewritingCfInstructionDesugaringEventConsumer.java
@@ -427,4 +427,10 @@
assert parent.verifyNothingToFinalize();
return true;
}
+
+ @Override
+ public void acceptDesugaredLibraryBridge(ProgramMethod method, ProgramMethod context) {
+ additionsCollection.addMethodAndHolderIfContextIsInProfile(method, context);
+ parent.acceptDesugaredLibraryBridge(method, context);
+ }
}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 4b80427..4707606 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -98,6 +98,8 @@
generator.forSingleMethodWithGlobalMerging("ApiModelOutline");
public final SyntheticKind API_MODEL_OUTLINE_WITHOUT_GLOBAL_MERGING =
generator.forSingleMethod("ApiModelOutline");
+ public final SyntheticKind DESUGARED_LIBRARY_BRIDGE =
+ generator.forSingleMethod("DesugaredLibraryBridge");
private final List<SyntheticKind> ALL_KINDS;
private String lazyVersionHash = null;
diff --git a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
index c1817a7..e5c2c66 100644
--- a/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
+++ b/src/main/java/com/android/tools/r8/utils/DeterminismChecker.java
@@ -153,12 +153,16 @@
public boolean onLine(String unescapedLine) throws IOException {
String line = escape(unescapedLine);
String dumpLine = reader.readLine();
- boolean equals = dumpLine.equals(line);
- if (!equals) {
- throw new AssertionError(
- "\nMismatch for line: " + line + "\n" + " and dump-line: " + dumpLine);
+ if (!dumpLine.equals(line)) {
+ // The line might contain a non unicode points. If so, the dump line will have mapped those
+ // to ? when writing. Decode the string and retry equals.
+ String decoded = new String(line.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+ if (!decoded.equals(dumpLine)) {
+ throw new AssertionError(
+ "\nMismatch for line: " + decoded + "\n" + " and dump-line: " + dumpLine);
+ }
}
- return equals;
+ return true;
}
@Override
diff --git a/src/main/keep_r8resourceshrinker.txt b/src/main/keep_r8resourceshrinker.txt
new file mode 100644
index 0000000..72bc436
--- /dev/null
+++ b/src/main/keep_r8resourceshrinker.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+-keepclassmembers enum com.android.resources.ResourceType { public static **[] values(); }
+-keepclassmembers enum com.android.resources.ResourceFolderType { public static **[] values(); }
diff --git a/src/main/resourceshrinker_cli.txt b/src/main/resourceshrinker_cli.txt
new file mode 100644
index 0000000..6273a47
--- /dev/null
+++ b/src/main/resourceshrinker_cli.txt
@@ -0,0 +1,10 @@
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+-keepclassmembers enum com.android.resources.ResourceType { public static **[] values(); }
+-keepclassmembers enum com.android.resources.ResourceFolderType { public static **[] values(); }
+
+-keep class com.android.build.shrinker.ResourceShrinkerCli {
+ public static void main(java.lang.String[]);
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java b/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java
new file mode 100644
index 0000000..3a3e3a6
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/DummyContent.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+public class DummyContent {
+
+ // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
+ public static final byte[] TINY_PNG =
+ new byte[] {
+ (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+ (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+ (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 1,
+ (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 58,
+ (byte) 126, (byte) -101, (byte) 85, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 10, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+ (byte) -38, (byte) 99, (byte) 96, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 2, (byte) 0, (byte) 1, (byte) -27, (byte) 39, (byte) -34,
+ (byte) -4, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 73,
+ (byte) 69, (byte) 78, (byte) 68, (byte) -82, (byte) 66, (byte) 96,
+ (byte) -126
+ };
+
+ public static final long TINY_PNG_CRC = 0x88b2a3b0L;
+
+ // A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
+ public static final byte[] TINY_9PNG =
+ new byte[] {
+ (byte) -119, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte) 10,
+ (byte) 26, (byte) 10, (byte) 0, (byte) 0, (byte) 0, (byte) 13,
+ (byte) 73, (byte) 72, (byte) 68, (byte) 82, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 3,
+ (byte) 8, (byte) 6, (byte) 0, (byte) 0, (byte) 0, (byte) 86,
+ (byte) 40, (byte) -75, (byte) -65, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 20, (byte) 73, (byte) 68, (byte) 65, (byte) 84, (byte) 120,
+ (byte) -38, (byte) 99, (byte) 96, (byte) -128, (byte) -128, (byte) -1,
+ (byte) 12, (byte) 48, (byte) 6, (byte) 8, (byte) -96, (byte) 8,
+ (byte) -128, (byte) 8, (byte) 0, (byte) -107, (byte) -111, (byte) 7,
+ (byte) -7, (byte) -64, (byte) -82, (byte) 8, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 73, (byte) 69, (byte) 78,
+ (byte) 68, (byte) -82, (byte) 66, (byte) 96, (byte) -126
+ };
+
+ public static final long TINY_9PNG_CRC = 0x1148f987L;
+
+ // The XML document <x/> as binary-packed with AAPT
+ public static final byte[] TINY_BINARY_XML =
+ new byte[] {
+ (byte) 3, (byte) 0, (byte) 8, (byte) 0, (byte) 104, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 28, (byte) 0,
+ (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 32, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 1,
+ (byte) 120, (byte) 0, (byte) 2, (byte) 1, (byte) 16, (byte) 0,
+ (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+ (byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+ (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 20, (byte) 0, (byte) 20, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 3, (byte) 1, (byte) 16, (byte) 0,
+ (byte) 24, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+ (byte) 0, (byte) 0, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+ (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0
+ };
+
+ public static final long TINY_BINARY_XML_CRC = 0xd7e65643L;
+
+ // The XML document <x/> as a proto packed with AAPT2
+ public static final byte[] TINY_PROTO_XML =
+ new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+ public static final long TINY_PROTO_XML_CRC = 3204905971L;
+
+ // The XML document <x/> as binary-packed with AAPT
+ public static final byte[] TINY_PROTO_CONVERTED_TO_BINARY_XML =
+ new byte[] {
+ (byte) 3, (byte) 0, (byte) 8, (byte) 0, (byte) 112, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 28, (byte) 0,
+ (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 32, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 1,
+ (byte) 120, (byte) 0, (byte) -128, (byte) 1, (byte) 8, (byte) 0,
+ (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 2, (byte) 1,
+ (byte) 16, (byte) 0, (byte) 36, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) -1, (byte) -1,
+ (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 0,
+ (byte) 20, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 1,
+ (byte) 16, (byte) 0, (byte) 24, (byte) 0, (byte) 0, (byte) 0,
+ (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) -1, (byte) -1,
+ (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1, (byte) -1,
+ (byte) 0, (byte) 0, (byte) 0, (byte) 0
+ };
+
+ private DummyContent() {}
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java b/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java
new file mode 100644
index 0000000..b96712c
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/LinkedResourcesFormat.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+public enum LinkedResourcesFormat {
+ BINARY,
+ PROTO,
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
new file mode 100644
index 0000000..5089061
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/PossibleResourcesMarker.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.lang.Character.isDigit;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.ide.common.resources.usage.ResourceUsageModel;
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
+import com.android.resources.ResourceType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Streams;
+import com.google.common.primitives.Ints;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Stream;
+
+/** Marks resources which are possibly referenced by string constants as reachable. */
+public class PossibleResourcesMarker {
+
+ // Copied from StringFormatDetector
+ // See java.util.Formatter docs
+ public static final Pattern FORMAT =
+ Pattern.compile(
+ // Generic format:
+ // %[argument_index$][flags][width][.precision]conversion
+ //
+ "%"
+ +
+ // Argument Index
+ "(\\d+\\$)?"
+ +
+ // Flags
+ "([-+#, 0(<]*)?"
+ +
+ // Width
+ "(\\d+)?"
+ +
+ // Precision
+ "(\\.\\d+)?"
+ +
+ // Conversion. These are all a single character, except date/time
+ // conversions
+ // which take a prefix of t/T:
+ "([tT])?"
+ +
+ // The current set of conversion characters are
+ // b,h,s,c,d,o,x,e,f,g,a,t (as well as all those as upper-case
+ // characters), plus
+ // n for newlines and % as a literal %. And then there are all the
+ // time/date
+ // characters: HIKLm etc. Just match on all characters here since there
+ // should
+ // be at least one.
+ "([a-zA-Z%])");
+
+ static final String NO_MATCH = "-nomatch-";
+
+ private final ShrinkerDebugReporter debugReporter;
+ private final ResourceStore resourceStore;
+ private final Set<String> strings;
+ private final boolean foundWebContent;
+
+ public PossibleResourcesMarker(ShrinkerDebugReporter debugReporter,
+ ResourceStore resourceStore,
+ Set<String> strings,
+ boolean foundWebContent) {
+ this.debugReporter = debugReporter;
+ this.resourceStore = resourceStore;
+ this.strings = strings;
+ this.foundWebContent = foundWebContent;
+ }
+
+ public void markPossibleResourcesReachable() {
+ Set<String> names =
+ resourceStore.getResources().stream()
+ .map(resource -> resource.name)
+ .collect(toImmutableSet());
+
+ int shortest = names.stream().mapToInt(String::length).min().orElse(Integer.MAX_VALUE);
+
+ // Check whether the string looks relevant
+ // We consider four types of strings:
+ // (1) simple resource names, e.g. "foo" from @layout/foo
+ // These might be the parameter to a getIdentifier() call, or could
+ // be composed into a fully qualified resource name for the getIdentifier()
+ // method. We match these for *all* resource types.
+ // (2) Relative source names, e.g. layout/foo, from @layout/foo
+ // These might be composed into a fully qualified resource name for
+ // getIdentifier().
+ // (3) Fully qualified resource names of the form package:type/name.
+ // (4) If foundWebContent is true, look for android_res/ URL strings as well
+ strings.stream()
+ .filter(string -> string.length() >= shortest)
+ .flatMap(
+ string -> {
+ int n = string.length();
+ boolean justName = true;
+ boolean formatting = false;
+ boolean haveSlash = false;
+ for (int i = 0; i < n; i++) {
+ char c = string.charAt(i);
+ haveSlash |= c == '/';
+ formatting |= c == '%';
+ justName =
+ justName && !(c == '.' || c == ':' || c == '%' || c == '/');
+ }
+
+ Stream<Resource> reachable = Streams.concat(
+ foundWebContent
+ ? possibleWebResources(names, string)
+ : Stream.empty(),
+ justName ? possiblePrefixMatch(string) : Stream.empty(),
+ formatting && !haveSlash
+ ? possibleFormatting(string)
+ : Stream.empty(),
+ haveSlash
+ ? possibleTypedResource(names, string)
+ : Stream.empty(),
+ possibleIntResource(string));
+
+ return reachable
+ .peek(resource -> debugReporter.debug(() -> "Marking "
+ + resource + " used because it matches string pool constant "
+ + string));
+ })
+ .forEach(ResourceUsageModel::markReachable);
+ }
+
+ private Stream<Resource> possibleWebResources(
+ Set<String> names, String string) {
+ // Look for android_res/ URL strings.
+ List<Resource> resources = resourceStore.getResourcesFromWebUrl(string);
+ if (!resources.isEmpty()) {
+ return resources.stream();
+ }
+
+ int start = Math.max(string.lastIndexOf('/'), 0);
+ int dot = string.indexOf('.', start);
+ String name = string.substring(start, dot != -1 ? dot : string.length());
+
+ if (names.contains(name)) {
+ return resourceStore.getResourceMaps().stream()
+ .filter(map -> map.containsKey(name))
+ .flatMap(map -> map.get(name).stream());
+ }
+ return Stream.empty();
+ }
+
+ private Stream<Resource> possiblePrefixMatch(String string) {
+ // Check for a simple prefix match, e.g. as in
+ // getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
+ return resourceStore.getResources().stream()
+ .filter(resource -> resource.name.startsWith(string));
+ }
+
+ private Stream<Resource> possibleFormatting(String string) {
+ // Possibly a formatting string, e.g.
+ // String name = String.format("my_prefix_%1d", index);
+ // int res = getContext().getResources().getIdentifier(name, "drawable", ...)
+ try {
+ Pattern pattern = Pattern.compile(convertFormatStringToRegexp(string));
+ return resourceStore.getResources().stream()
+ .filter(resource -> pattern.matcher(resource.name).matches());
+ } catch (PatternSyntaxException ignored) {
+ return Stream.empty();
+ }
+ }
+
+ private Stream<Resource> possibleTypedResource(
+ Set<String> names, String string) {
+ // Try to pick out the resource name pieces; if we can find the
+ // resource type unambiguously; if not, just match on names
+ int slash = string.indexOf('/');
+ String name = string.substring(slash + 1);
+ if (name.isEmpty() || !names.contains(name)) {
+ return Stream.empty();
+ }
+ // See if have a known specific resource type
+ if (slash > 0) {
+ int colon = string.indexOf(':');
+ String typeName = string.substring(colon + 1, slash);
+ ResourceType type = ResourceType.fromClassName(typeName);
+ return type != null
+ ? resourceStore.getResources(type, name).stream()
+ : Stream.empty();
+ }
+ return resourceStore.getResourceMaps().stream()
+ .filter(map -> map.containsKey(name))
+ .flatMap(map -> map.get(name).stream());
+ }
+
+ private Stream<Resource> possibleIntResource(String string) {
+ // Just a number? There are cases where it calls getIdentifier by
+ // a String number; see for example SuggestionsAdapter in the support
+ // library which reports supporting a string like "2130837524" and
+ // "android.resource://com.android.alarmclock/2130837524".
+ String withoutSlash = string.substring(string.lastIndexOf('/') + 1);
+ if (withoutSlash.isEmpty() || !isDigit(withoutSlash.charAt(0))) {
+ return Stream.empty();
+ }
+ Integer id = Ints.tryParse(withoutSlash);
+ Resource resource = null;
+ if (id != null) {
+ resource = resourceStore.getResource(id);
+ }
+ return resource != null ? Stream.of(resource) : Stream.empty();
+ }
+
+ @VisibleForTesting
+ static String convertFormatStringToRegexp(String formatString) {
+ StringBuilder regexp = new StringBuilder();
+ int from = 0;
+ boolean hasEscapedLetters = false;
+ Matcher matcher = FORMAT.matcher(formatString);
+ int length = formatString.length();
+ while (matcher.find(from)) {
+ int start = matcher.start();
+ int end = matcher.end();
+ if (start == 0 && end == length) {
+ // Don't match if the entire string literal starts with % and ends with
+ // the a formatting character, such as just "%d": this just matches absolutely
+ // everything and is unlikely to be used in a resource lookup
+ return NO_MATCH;
+ }
+ if (start > from) {
+ hasEscapedLetters |= appendEscapedPattern(formatString, regexp, from, start);
+ }
+ String pattern = ".*";
+ String conversion = matcher.group(6);
+ String timePrefix = matcher.group(5);
+
+ //noinspection VariableNotUsedInsideIf,StatementWithEmptyBody: for readability.
+ if (timePrefix != null) {
+ // date notation; just use .* to match these
+ } else if (conversion != null && conversion.length() == 1) {
+ char type = conversion.charAt(0);
+ switch (type) {
+ case 's':
+ case 'S':
+ case 't':
+ case 'T':
+ // Match everything
+ break;
+ case '%':
+ pattern = "%";
+ break;
+ case 'n':
+ pattern = "\n";
+ break;
+ case 'c':
+ case 'C':
+ pattern = ".";
+ break;
+ case 'x':
+ case 'X':
+ pattern = "\\p{XDigit}+";
+ break;
+ case 'd':
+ case 'o':
+ pattern = "\\p{Digit}+";
+ break;
+ case 'b':
+ pattern = "(true|false)";
+ break;
+ case 'B':
+ pattern = "(TRUE|FALSE)";
+ break;
+ case 'h':
+ case 'H':
+ pattern = "(null|\\p{XDigit}+)";
+ break;
+ case 'f':
+ pattern = "-?[\\p{XDigit},.]+";
+ break;
+ case 'e':
+ pattern = "-?\\p{Digit}+[,.]\\p{Digit}+e\\+?\\p{Digit}+";
+ break;
+ case 'E':
+ pattern = "-?\\p{Digit}+[,.]\\p{Digit}+E\\+?\\p{Digit}+";
+ break;
+ case 'a':
+ pattern = "0x[\\p{XDigit},.+p]+";
+ break;
+ case 'A':
+ pattern = "0X[\\p{XDigit},.+P]+";
+ break;
+ case 'g':
+ case 'G':
+ pattern = "-?[\\p{XDigit},.+eE]+";
+ break;
+ }
+
+ // Allow space or 0 prefix
+ if (!".*".equals(pattern)) {
+ String width = matcher.group(3);
+ //noinspection VariableNotUsedInsideIf
+ if (width != null) {
+ String flags = matcher.group(2);
+ if ("0".equals(flags)) {
+ pattern = "0*" + pattern;
+ } else {
+ pattern = " " + pattern;
+ }
+ }
+ }
+
+ // If it's a general .* wildcard which follows a previous .* wildcard,
+ // just skip it (e.g. don't convert %s%s into .*.*; .* is enough.)
+ int regexLength = regexp.length();
+ if (!".*".equals(pattern)
+ || regexLength < 2
+ || regexp.charAt(regexLength - 1) != '*'
+ || regexp.charAt(regexLength - 2) != '.') {
+ regexp.append(pattern);
+ }
+ }
+ from = end;
+ }
+
+ if (from < length) {
+ hasEscapedLetters |= appendEscapedPattern(formatString, regexp, from, length);
+ }
+
+ if (!hasEscapedLetters) {
+ // If the regexp contains *only* formatting characters, e.g. "%.0f%d", or
+ // if it contains only formatting characters and punctuation, e.g. "%s_%d",
+ // don't treat this as a possible resource name pattern string: it is unlikely
+ // to be intended for actual resource names, and has the side effect of matching
+ // most names.
+ return NO_MATCH;
+ }
+
+ return regexp.toString();
+ }
+
+ /**
+ * Appends the characters in the range [from,to> from formatString as escaped regexp characters
+ * into the given string builder. Returns true if there were any letters in the appended text.
+ */
+ private static boolean appendEscapedPattern(
+ @NonNull String formatString, @NonNull StringBuilder regexp, int from, int to) {
+ regexp.append(Pattern.quote(formatString.substring(from, to)));
+
+ for (int i = from; i < to; i++) {
+ if (Character.isLetter(formatString.charAt(i))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java
new file mode 100644
index 0000000..e38056a
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinker.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.annotations.NonNull;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+
+/**
+ * Interface for unit that analyzes all resources (after resource merging, compilation and code
+ * shrinking has been completed) and figures out which resources are unused, and replaces them with
+ * dummy content inside zip archive file.
+ */
+public interface ResourceShrinker extends AutoCloseable {
+
+ /**
+ * Analyzes resources and detects unreachable ones. It includes the following steps:
+ *
+ * <ul>
+ * <li>Gather resources available in project.
+ * <li>Record resource usages via analyzing compiled code, AndroidManifest etc.
+ * <li>Build reference graph and connect dependent resources.
+ * <li>Detects WebView and/or {@code Resources#getIdentifier} usages and guess which
+ * resources are reachable analyzing string constants available in compiled code.
+ * <li>Processes resources explicitly asked to keep and discard. <tools:keep> and
+ * <tools:discard> attribute.
+ * <li>Based on the root references computes unreachable resources.
+ * </ul>
+ */
+ void analyze() throws IOException, ParserConfigurationException, SAXException;
+
+ /**
+ * Returns count of unused resources. Should be called after {@code ResourceShrinker#analyze}.
+ */
+ int getUnusedResourceCount();
+
+ /**
+ * Replaces entries in {@param source} zip archive that belong to unused resources with dummy
+ * content and produces a new {@param dest} zip archive. Zip archive should contain resources
+ * in 'res/' folder like it is stored in APK.
+ *
+ * <p>For now, doesn't change resource table and applies to file-based resources like layouts,
+ * menus and drawables, not value-based resources like strings and dimensions.
+ *
+ * <p>Should be called after {@code ResourceShrinker#analyze}.
+ */
+ void rewriteResourcesInApkFormat(
+ @NonNull File source,
+ @NonNull File dest,
+ @NonNull LinkedResourcesFormat format
+ ) throws IOException;
+
+ /**
+ * Replaces entries in {@param source} zip archive that belong to unused resources with dummy
+ * content and produces a new {@param dest} zip archive. Zip archive represents App Bundle which
+ * may have multiple modules and each module has its own resource directory: '${module}/res/'.
+ * Package name for each bundle module should be passed as {@param moduleToPackageName}.
+ *
+ * <p>For now, doesn't change resource table and applies to file-based resources like layouts,
+ * menus and drawables, not value-based resources like strings and dimensions.
+ *
+ * <p>Should be called after {@code ResourceShrinker#analyze}.
+ */
+ void rewriteResourcesInBundleFormat(
+ @NonNull File source,
+ @NonNull File dest,
+ @NonNull Map<String, String> moduleToPackageName
+ ) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
new file mode 100644
index 0000000..3e30568
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerCli.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.build.shrinker.gatherer.ProtoResourceTableGatherer;
+import com.android.build.shrinker.gatherer.ResourcesGatherer;
+import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
+import com.android.build.shrinker.usages.DexUsageRecorder;
+import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorder;
+import com.android.build.shrinker.usages.ResourceUsageRecorder;
+import com.android.build.shrinker.usages.ToolsAttributeUsageRecorder;
+import com.android.utils.FileUtils;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipFile;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+
+public class ResourceShrinkerCli {
+
+ private static final String INPUT_ARG = "--input";
+ private static final String DEX_INPUT_ARG = "--dex_input";
+ private static final String OUTPUT_ARG = "--output";
+ private static final String RES_ARG = "--raw_resources";
+ private static final String HELP_ARG = "--help";
+ private static final String PRINT_USAGE_LOG = "--print_usage_log";
+
+ private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
+ private static final String RESOURCES_PB = "resources.pb";
+ private static final String RES_FOLDER = "res";
+
+ private static class Options {
+ private String input;
+ private final List<String> dex_inputs = new ArrayList<>();
+ private String output;
+ private String usageLog;
+ private final List<String> rawResources = new ArrayList<>();
+ private boolean help;
+
+ private Options() {}
+
+ public static Options parseOptions(String[] args) {
+ Options options = new Options();
+ for (int i = 0; i < args.length; i++) {
+ String arg = args[i];
+ if (arg.startsWith(INPUT_ARG)) {
+ i++;
+ if (i == args.length) {
+ throw new ResourceShrinkingFailedException("No argument given for input");
+ }
+ if (options.input != null) {
+ throw new ResourceShrinkingFailedException(
+ "More than one input not supported");
+ }
+ options.input = args[i];
+ } else if (arg.startsWith(OUTPUT_ARG)) {
+ i++;
+ if (i == args.length) {
+ throw new ResourceShrinkingFailedException("No argument given for output");
+ }
+ if (options.output != null) {
+ throw new ResourceShrinkingFailedException(
+ "More than one output not supported");
+ }
+ options.output = args[i];
+ } else if (arg.startsWith(DEX_INPUT_ARG)) {
+ i++;
+ if (i == args.length) {
+ throw new ResourceShrinkingFailedException(
+ "No argument given for dex_input");
+ }
+ options.dex_inputs.add(args[i]);
+ } else if (arg.startsWith(PRINT_USAGE_LOG)) {
+ i++;
+ if (i == args.length) {
+ throw new ResourceShrinkingFailedException(
+ "No argument given for usage log");
+ }
+ if (options.usageLog != null) {
+ throw new ResourceShrinkingFailedException(
+ "More than usage log not supported");
+ }
+ options.usageLog = args[i];
+ } else if (arg.startsWith(RES_ARG)) {
+ i++;
+ if (i == args.length) {
+ throw new ResourceShrinkingFailedException(
+ "No argument given for raw_resources");
+ }
+ options.rawResources.add(args[i]);
+ } else if (arg.equals(HELP_ARG)) {
+ options.help = true;
+ } else {
+ throw new ResourceShrinkingFailedException("Unknown argument " + arg);
+ }
+ }
+ return options;
+ }
+
+ public String getInput() {
+ return input;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public String getUsageLog() {
+ return usageLog;
+ }
+
+ public List<String> getRawResources() {
+ return rawResources;
+ }
+
+ public boolean isHelp() {
+ return help;
+ }
+ }
+
+ public static void main(String[] args) {
+ run(args);
+ }
+
+ protected static ResourceShrinkerImpl run(String[] args) {
+ try {
+ Options options = Options.parseOptions(args);
+ if (options.isHelp()) {
+ printUsage();
+ return null;
+ }
+ validateOptions(options);
+ ResourceShrinkerImpl resourceShrinker = runResourceShrinking(options);
+ return resourceShrinker;
+ } catch (IOException | ParserConfigurationException | SAXException e) {
+ throw new ResourceShrinkingFailedException(
+ "Failed running resource shrinking: " + e.getMessage(), e);
+ }
+ }
+
+ private static ResourceShrinkerImpl runResourceShrinking(Options options)
+ throws IOException, ParserConfigurationException, SAXException {
+ validateInput(options.getInput());
+ List<ResourceUsageRecorder> resourceUsageRecorders = new ArrayList<>();
+ for (String dexInput : options.dex_inputs) {
+ validateFileExists(dexInput);
+ resourceUsageRecorders.add(
+ new DexUsageRecorder(
+ FileUtils.createZipFilesystem(Paths.get(dexInput)).getPath("")));
+ }
+ Path protoApk = Paths.get(options.getInput());
+ Path protoApkOut = Paths.get(options.getOutput());
+ FileSystem fileSystemProto = FileUtils.createZipFilesystem(protoApk);
+ resourceUsageRecorders.add(new DexUsageRecorder(fileSystemProto.getPath("")));
+ resourceUsageRecorders.add(
+ new ProtoAndroidManifestUsageRecorder(
+ fileSystemProto.getPath(ANDROID_MANIFEST_XML)));
+ // If the apk contains a raw folder, find keep rules in there
+ if (new ZipFile(options.getInput())
+ .stream().anyMatch(zipEntry -> zipEntry.getName().startsWith("res/raw"))) {
+ Path rawPath = fileSystemProto.getPath("res", "raw");
+ resourceUsageRecorders.add(new ToolsAttributeUsageRecorder(rawPath));
+ }
+ ResourcesGatherer gatherer =
+ new ProtoResourceTableGatherer(fileSystemProto.getPath(RESOURCES_PB));
+ ProtoResourcesGraphBuilder res =
+ new ProtoResourcesGraphBuilder(
+ fileSystemProto.getPath(RES_FOLDER), fileSystemProto.getPath(RESOURCES_PB));
+ ResourceShrinkerImpl resourceShrinker =
+ new ResourceShrinkerImpl(
+ List.of(gatherer),
+ null,
+ resourceUsageRecorders,
+ List.of(res),
+ options.usageLog != null
+ ? new FileReporter(Paths.get(options.usageLog).toFile())
+ : NoDebugReporter.INSTANCE,
+ false, // TODO(b/245721267): Add support for bundles
+ true);
+ resourceShrinker.analyze();
+
+ resourceShrinker.rewriteResourcesInApkFormat(
+ protoApk.toFile(), protoApkOut.toFile(), LinkedResourcesFormat.PROTO);
+ return resourceShrinker;
+ }
+
+ private static void validateInput(String input) throws IOException {
+ ZipFile zipfile = new ZipFile(input);
+ if (zipfile.getEntry(ANDROID_MANIFEST_XML) == null) {
+ throw new ResourceShrinkingFailedException(
+ "Input must include " + ANDROID_MANIFEST_XML);
+ }
+ if (zipfile.getEntry(RESOURCES_PB) == null) {
+ throw new ResourceShrinkingFailedException(
+ "Input must include "
+ + RESOURCES_PB
+ + ". Did you not convert the input apk"
+ + " to proto?");
+ }
+ if (zipfile.stream().noneMatch(zipEntry -> zipEntry.getName().startsWith(RES_FOLDER))) {
+ throw new ResourceShrinkingFailedException(
+ "Input must include a " + RES_FOLDER + " folder");
+ }
+ }
+
+ private static void validateFileExists(String file) {
+ if (!Paths.get(file).toFile().exists()) {
+ throw new RuntimeException("Can't find file: " + file);
+ }
+ }
+
+ private static void validateOptions(Options options) {
+ if (options.getInput() == null) {
+ throw new ResourceShrinkingFailedException("No input given.");
+ }
+ if (options.getOutput() == null) {
+ throw new ResourceShrinkingFailedException("No output destination given.");
+ }
+ validateFileExists(options.getInput());
+ for (String rawResource : options.getRawResources()) {
+ validateFileExists(rawResource);
+ }
+ }
+
+ private static void printUsage() {
+ PrintStream out = System.err;
+ out.println("Usage:");
+ out.println(" resourceshrinker ");
+ out.println(" --input <input-file>, container with manifest, resources table and res");
+ out.println(" folder. May contain dex.");
+ out.println(" --dex_input <input-file> Container with dex files (only dex will be ");
+ out.println(" handled if this contains other files. Several --dex_input arguments");
+ out.println(" are supported");
+ out.println(" --output <output-file>");
+ out.println(" --raw_resource <xml-file or res directory>");
+ out.println(" optional, more than one raw_resoures argument might be given");
+ out.println(" --help prints this help message");
+ }
+
+ private static class ResourceShrinkingFailedException extends RuntimeException {
+ public ResourceShrinkingFailedException(String message) {
+ super(message);
+ }
+
+ public ResourceShrinkingFailedException(String message, Exception e) {
+ super(message, e);
+ }
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
new file mode 100644
index 0000000..1136085
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker
+
+import com.android.SdkConstants.DOT_9PNG
+import com.android.SdkConstants.DOT_PNG
+import com.android.SdkConstants.DOT_XML
+import com.android.aapt.Resources
+import com.android.build.shrinker.DummyContent.TINY_9PNG
+import com.android.build.shrinker.DummyContent.TINY_9PNG_CRC
+import com.android.build.shrinker.DummyContent.TINY_BINARY_XML
+import com.android.build.shrinker.DummyContent.TINY_BINARY_XML_CRC
+import com.android.build.shrinker.DummyContent.TINY_PNG
+import com.android.build.shrinker.DummyContent.TINY_PNG_CRC
+import com.android.build.shrinker.DummyContent.TINY_PROTO_XML
+import com.android.build.shrinker.DummyContent.TINY_PROTO_XML_CRC
+import com.android.build.shrinker.gatherer.ResourcesGatherer
+import com.android.build.shrinker.graph.ResourcesGraphBuilder
+import com.android.build.shrinker.obfuscation.ObfuscationMappingsRecorder
+import com.android.build.shrinker.usages.ResourceUsageRecorder
+import com.android.ide.common.resources.findUnusedResources
+import com.android.ide.common.resources.usage.ResourceStore
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource
+import com.android.resources.FolderTypeRelationship
+import com.android.resources.ResourceFolderType
+import com.android.resources.ResourceType
+import com.google.common.io.ByteStreams
+import com.google.common.io.Files
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+import java.util.zip.CRC32
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+
+/**
+ * Unit that analyzes all resources (after resource merging, compilation and code shrinking has
+ * been completed) and figures out which resources are unused, and replaces them with dummy content
+ * inside zip archive file.
+ *
+ * Resource shrinker implementation that allows to customize:
+ * <ul>
+ * <li>application resource gatherer (from R files, resource tables, etc);
+ * <li>recorder for mappings from obfuscated class/methods to original class/methods;
+ * <li>sources from which resource usages are recorded (Dex files, compiled JVM classes,
+ * AndroidManifests, etc);
+ * <li>resources graph builder that connects resources dependent on each other (analyzing
+ * raw resources content in XML, HTML, CSS, JS, analyzing resource content in proto
+ * compiled format);
+ * </ul>
+ */
+class ResourceShrinkerImpl(
+ private val resourcesGatherers: List<ResourcesGatherer>,
+ private val obfuscationMappingsRecorder: ObfuscationMappingsRecorder?,
+ private val usageRecorders: List<ResourceUsageRecorder>,
+ private val graphBuilders: List<ResourcesGraphBuilder>,
+ private val debugReporter: ShrinkerDebugReporter,
+ val supportMultipackages: Boolean,
+ private val usePreciseShrinking: Boolean
+) : ResourceShrinker {
+ val model = ResourceShrinkerModel(debugReporter, supportMultipackages)
+ private lateinit var unused: List<Resource>
+
+ override fun analyze() {
+ resourcesGatherers.forEach { it.gatherResourceValues(model) }
+ obfuscationMappingsRecorder?.recordObfuscationMappings(model)
+ usageRecorders.forEach { it.recordUsages(model) }
+ graphBuilders.forEach { it.buildGraph(model) }
+
+ model.resourceStore.processToolsAttributes()
+ model.keepPossiblyReferencedResources()
+
+ debugReporter.debug { model.resourceStore.dumpResourceModel() }
+
+ unused = findUnusedResources(model.resourceStore.resources) { roots ->
+ debugReporter.debug { "The root reachable resources are:" }
+ debugReporter.debug { roots.joinToString("\n", transform = { " $it" }) }
+ }
+ debugReporter.debug { "Unused resources are: " }
+ debugReporter.debug { unused.joinToString("\n", transform = { " $it" })}
+
+ }
+
+ override fun close() {
+ debugReporter.close()
+ }
+
+ override fun getUnusedResourceCount(): Int {
+ return unused.size
+ }
+
+ override fun rewriteResourcesInApkFormat(
+ source: File,
+ dest: File,
+ format: LinkedResourcesFormat
+ ) {
+ rewriteResourceZip(source, dest, ApkArchiveFormat(model.resourceStore, format))
+ }
+
+ override fun rewriteResourcesInBundleFormat(
+ source: File,
+ dest: File,
+ moduleNameToPackageNameMap: Map<String, String>
+ ) {
+ rewriteResourceZip(
+ source,
+ dest,
+ BundleArchiveFormat(model.resourceStore, moduleNameToPackageNameMap)
+ )
+ }
+
+ private fun rewriteResourceZip(source: File, dest: File, format: ArchiveFormat) {
+ if (dest.exists() && !dest.delete()) {
+ throw IOException("Could not delete $dest")
+ }
+ JarOutputStream(BufferedOutputStream(FileOutputStream(dest))).use { zos ->
+ ZipFile(source).use { zip ->
+ // Rather than using Deflater.DEFAULT_COMPRESSION we use 9 here, since that seems
+ // to match the compressed sizes we observe in source .ap_ files encountered by the
+ // resource shrinker:
+ zos.setLevel(9)
+ zip.entries().asSequence().forEach {
+ if (format.fileIsNotReachable(it)) {
+ // If we don't use precise shrinking we don't remove the files, see:
+ // https://b.corp.google.com/issues/37010152
+ if (!usePreciseShrinking) {
+ replaceWithDummyEntry(zos, it, format.resourcesFormat)
+ }
+ } else if (it.name.endsWith("resources.pb") && usePreciseShrinking) {
+ removeResourceUnusedTableEntries(zip.getInputStream(it), zos, it)
+ } else {
+ copyToOutput(zip.getInputStream(it), zos, it)
+ }
+ }
+ }
+ }
+ // If net negative, copy original back. This is unusual, but can happen
+ // in some circumstances, such as the one described in
+ // https://plus.google.com/+SaidTahsinDane/posts/X9sTSwoVUhB
+ // "Removed unused resources: Binary resource data reduced from 588KB to 595KB: Removed -1%"
+ // Guard against that, and worst case, just use the original.
+ val before = source.length()
+ val after = dest.length()
+ if (after > before) {
+ debugReporter.info {
+ "Resource shrinking did not work (grew from $before to $after); using original " +
+ "instead"
+ }
+ Files.copy(source, dest)
+ }
+ }
+
+ private fun removeResourceUnusedTableEntries(zis: InputStream,
+ zos: JarOutputStream,
+ srcEntry: ZipEntry) {
+ val resourceIdsToRemove =
+ model.resourceStore.resources.filter { !it.isReachable }.map { it.value }.toList()
+ val shrunkenResourceTable = Resources.ResourceTable.parseFrom(zis)
+ .nullOutEntriesWithIds(resourceIdsToRemove)
+ val bytes = shrunkenResourceTable.toByteArray()
+ val outEntry = JarEntry(srcEntry.name)
+ if (srcEntry.time != -1L) {
+ outEntry.time = srcEntry.time
+ }
+ if (srcEntry.method == JarEntry.STORED) {
+ outEntry.method = JarEntry.STORED
+ outEntry.size = bytes.size.toLong()
+ val crc = CRC32()
+ crc.update(bytes, 0, bytes.size)
+ outEntry.crc = crc.getValue()
+ }
+ zos.putNextEntry(outEntry)
+ zos.write(bytes)
+ zos.closeEntry()
+ }
+
+ /** Replaces the given entry with a minimal valid file of that type. */
+ private fun replaceWithDummyEntry(
+ zos: JarOutputStream,
+ entry: ZipEntry,
+ format: LinkedResourcesFormat
+ ) {
+ // Create a new entry so that the compressed len is recomputed.
+ val name = entry.name
+ val (bytes, crc) = when {
+ // DOT_9PNG (.9.png) must be always before DOT_PNG (.png)
+ name.endsWith(DOT_9PNG) -> TINY_9PNG to TINY_9PNG_CRC
+ name.endsWith(DOT_PNG) -> TINY_PNG to TINY_PNG_CRC
+ name.endsWith(DOT_XML) && format == LinkedResourcesFormat.BINARY ->
+ TINY_BINARY_XML to TINY_BINARY_XML_CRC
+ name.endsWith(DOT_XML) && format == LinkedResourcesFormat.PROTO ->
+ TINY_PROTO_XML to TINY_PROTO_XML_CRC
+ else -> ByteArray(0) to 0L
+ }
+
+ val outEntry = JarEntry(name)
+ if (entry.time != -1L) {
+ outEntry.time = entry.time
+ }
+ if (entry.method == JarEntry.STORED) {
+ outEntry.method = JarEntry.STORED
+ outEntry.size = bytes.size.toLong()
+ outEntry.crc = crc
+ }
+ zos.putNextEntry(outEntry)
+ zos.write(bytes)
+ zos.closeEntry()
+ debugReporter.info {
+ "Skipped unused resource $name: ${entry.size} bytes (replaced with small dummy file " +
+ "of size ${bytes.size} bytes)"
+ }
+ }
+
+ private fun copyToOutput(zis: InputStream, zos: JarOutputStream, entry: ZipEntry) {
+ // We can't just compress all files; files that are not compressed in the source .ap_ file
+ // must be left uncompressed here, since for example RAW files need to remain uncompressed
+ // in the APK such that they can be mmap'ed at runtime.
+ // Preserve the STORED method of the input entry.
+ val outEntry = when (entry.method) {
+ JarEntry.STORED -> JarEntry(entry)
+ else -> JarEntry(entry.name)
+ }
+ if (entry.time != -1L) {
+ outEntry.time = entry.time
+ }
+ zos.putNextEntry(outEntry)
+ if (!entry.isDirectory) {
+ zos.write(ByteStreams.toByteArray(zis))
+ }
+ zos.closeEntry()
+ }
+}
+
+private interface ArchiveFormat {
+ val resourcesFormat: LinkedResourcesFormat
+ fun fileIsNotReachable(entry: ZipEntry): Boolean
+}
+
+private class ApkArchiveFormat(
+ private val store: ResourceStore,
+ override val resourcesFormat: LinkedResourcesFormat
+) : ArchiveFormat {
+
+ override fun fileIsNotReachable(entry: ZipEntry): Boolean {
+ if (entry.isDirectory || !entry.name.startsWith("res/")) {
+ return false
+ }
+ val (_, folder, name) = entry.name.split('/', limit = 3)
+ return !store.isJarPathReachable(folder, name)
+ }
+}
+
+private class BundleArchiveFormat(
+ private val store: ResourceStore,
+ private val moduleNameToPackageName: Map<String, String>
+) : ArchiveFormat {
+
+ override val resourcesFormat = LinkedResourcesFormat.PROTO
+
+ override fun fileIsNotReachable(entry: ZipEntry): Boolean {
+ val module = entry.name.substringBefore('/')
+ val packageName = moduleNameToPackageName[module]
+ if (entry.isDirectory || packageName == null || !entry.name.startsWith("$module/res/")) {
+ return false
+ }
+ val (_, _, folder, name) = entry.name.split('/', limit = 4)
+ return !store.isJarPathReachable(folder, name)
+ }
+}
+
+private fun ResourceStore.isJarPathReachable(
+ folder: String,
+ name: String
+): Boolean {
+ val folderType = ResourceFolderType.getFolderType(folder) ?: return true
+ val resourceName = name.substringBefore('.')
+ // Bundle format has a restriction: in case the same resource is duplicated in multiple modules
+ // its content should be the same in all of them. This restriction means that we can't replace
+ // resource with dummy content if its duplicate is used in some module.
+ return FolderTypeRelationship.getRelatedResourceTypes(folderType)
+ .filterNot { it == ResourceType.ID }
+ .flatMap { getResources(it, resourceName) }
+ .any { it.isReachable }
+}
+
+private fun ResourceStore.getResourceId(
+ folder: String,
+ name: String
+): Int {
+ val folderType = ResourceFolderType.getFolderType(folder) ?: return -1
+ val resourceName = name.substringBefore('.')
+ return FolderTypeRelationship.getRelatedResourceTypes(folderType)
+ .filterNot { it == ResourceType.ID }
+ .flatMap { getResources(it, resourceName) }
+ .map { it.value }
+ .getOrElse(0) { -1 }
+
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java
new file mode 100644
index 0000000..ce1b39f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerModel.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker;
+
+import com.android.aapt.Resources.ResourceTable;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.build.shrinker.obfuscation.ObfuscatedClasses;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
+import com.android.resources.ResourceType;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Represents resource shrinker state that is shared between shrinker stages ResourcesGatherer,
+ * ObfuscationMappingsRecorder, ResourceUsageRecorder, ResourcesGraphBuilder and each stage
+ * contributes an information via changing its state.
+ */
+public class ResourceShrinkerModel {
+
+ private final ShrinkerDebugReporter debugReporter;
+
+ private final ResourceStore resourceStore;
+ private ObfuscatedClasses obfuscatedClasses = ObfuscatedClasses.NO_OBFUSCATION;
+
+ private final Set<String> strings = Sets.newHashSetWithExpectedSize(300);
+
+ private final Map<String, ResourceTable> resourceTableCache = Maps.newHashMap();
+
+ private boolean foundGetIdentifier = false;
+ private boolean foundWebContent = false;
+
+ public ResourceShrinkerModel(
+ ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
+ this.debugReporter = debugReporter;
+ resourceStore = new ResourceStore(supportMultipackages);
+ }
+
+ @NonNull
+ public ShrinkerDebugReporter getDebugReporter() {
+ return debugReporter;
+ }
+
+ @NonNull
+ public ResourceStore getResourceStore() {
+ return resourceStore;
+ }
+
+ @NonNull
+ public ObfuscatedClasses getObfuscatedClasses() {
+ return obfuscatedClasses;
+ }
+
+ /** Reports recorded mappings from obfuscated classes to original classes */
+ public void setObfuscatedClasses(@NonNull ObfuscatedClasses obfuscatedClasses) {
+ this.obfuscatedClasses = obfuscatedClasses;
+ }
+
+ /** Adds a new gathered resource to model. */
+ @NonNull
+ public Resource addResource(
+ @NonNull ResourceType type,
+ @Nullable String packageName,
+ @NonNull String name,
+ @Nullable String value) {
+ int intValue = -1;
+ try {
+ intValue = value != null ? Integer.decode(value) : -1;
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ return addResource(type, packageName, name, intValue);
+ }
+
+ /** Adds a new gathered resource to model. */
+ @NonNull
+ public Resource addResource(
+ @NonNull ResourceType type,
+ @Nullable String packageName,
+ @NonNull String name,
+ int value) {
+ return resourceStore.addResource(new Resource(packageName, type, name, value));
+ }
+
+ /** Adds string constant found in code to strings pool. */
+ public void addStringConstant(@NonNull String string) {
+ strings.add(string);
+ }
+
+ /** Returns is reference to {@code Resources#getIdentifier} is found in code. */
+ public boolean isFoundGetIdentifier() {
+ return foundGetIdentifier;
+ }
+
+ /** Reports that reference to {@code Resources#getIdentifier} is found in code. */
+ public void setFoundGetIdentifier(boolean foundGetIdentifier) {
+ this.foundGetIdentifier = foundGetIdentifier;
+ }
+
+ /** Returns is web content is found in code. */
+ public boolean isFoundWebContent() {
+ return foundWebContent;
+ }
+
+ /** Reports that reference to web content is found in code. */
+ public void setFoundWebContent(boolean foundWebContent) {
+ this.foundWebContent = foundWebContent;
+ }
+
+ /** Returns all string constants gathered from compiled classes. */
+ public Set<String> getStrings() {
+ return strings;
+ }
+
+ /**
+ * Mark resources that match string constants as reachable in case invocation of
+ * {@code Resources#getIdentifier} or web content is found in code and safe mode in enabled.
+ */
+ public void keepPossiblyReferencedResources() {
+ if (strings.isEmpty()
+ || !resourceStore.getSafeMode()
+ || (!foundGetIdentifier && !foundWebContent)) {
+ // No calls to android.content.res.Resources#getIdentifier; or user specifically asked
+ // for us not to guess resources to keep
+ return;
+ }
+
+ debugReporter.debug(() -> ""
+ + "android.content.res.Resources#getIdentifier present: " + foundGetIdentifier + "\n"
+ + "Web content present: " + foundWebContent + "\n"
+ + "Referenced Strings:\n"
+ + strings.stream()
+ .map(s -> s.trim().replace("\n", "\\n"))
+ .filter(s -> !s.isEmpty())
+ .map(s -> s.length() > 40 ? s.substring(0, 37) + "..." : s)
+ .collect(Collectors.joining("\n"))
+ );
+
+ new PossibleResourcesMarker(debugReporter, resourceStore, strings, foundWebContent)
+ .markPossibleResourcesReachable();
+ }
+
+ /**
+ * Reads resource table from specified path and stores it in cache to be able to reuse it if
+ * the same resource table is requested by another unit.
+ */
+ public ResourceTable readResourceTable(Path resourceTablePath) {
+ return resourceTableCache.computeIfAbsent(resourceTablePath.toString(), (path) -> {
+ try {
+ return ResourceTable.parseFrom(Files.readAllBytes(resourceTablePath));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
new file mode 100644
index 0000000..69952ef
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker
+
+import com.android.aapt.Resources
+
+internal fun Resources.ResourceTable.entriesSequence(): Sequence<EntryWrapper> = sequence {
+ for (resourcePackage in packageList) {
+ for (resourceType in resourcePackage.typeList) {
+ for (resourceEntry in resourceType.entryList) {
+ val id = toIdentifier(resourcePackage, resourceType, resourceEntry)
+ yield(
+ EntryWrapper(id, resourcePackage.packageName, resourceType.name, resourceEntry)
+ )
+ }
+ }
+ }
+}
+
+internal fun Resources.ResourceTable.nullOutEntriesWithIds(ids: List<Int>)
+ : Resources.ResourceTable {
+ if (ids.isEmpty()) {
+ return this
+ }
+ val packageMappings = calculatePackageMappings(ids)
+ val tableBuilder = this.toBuilder()
+ tableBuilder.packageBuilderList.forEach{
+ val typeMappings = packageMappings[it.packageId.id]
+ if (typeMappings != null) {
+ it.typeBuilderList.forEach { type->
+ val entryList = typeMappings[type.typeId.id]
+ if (entryList != null) {
+ type.entryBuilderList.forEach { entry ->
+ if (entryList.contains(entry.entryId.id)) {
+ entry.clearConfigValue()
+ if (entry.hasOverlayableItem()) {
+ entry.clearOverlayableItem()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return tableBuilder.build()
+}
+
+private fun calculatePackageMappings(ids: List<Int>): MutableMap<Int, Map<Int, List<Int>>> {
+ val sortedIds = ids.sorted()
+ val packageMapping = mutableMapOf<Int, Map<Int, List<Int>>>()
+ var typeMapping = mutableMapOf<Int, List<Int>>()
+ var entryList = mutableListOf<Int>()
+ var oldPackageId = -1
+ var oldTypeId = -1
+ for (value in sortedIds) {
+ val packageId = packageIdFromIdentifier(value)
+ val typeId = typeIdFromIdentifier(value)
+ val entryId = entryIdFromIdentifier(value)
+ if (packageId != oldPackageId) {
+ typeMapping = mutableMapOf()
+ packageMapping.put(packageId, typeMapping)
+ oldPackageId = packageId
+ oldTypeId = -1
+ }
+ if (typeId != oldTypeId) {
+ entryList = mutableListOf()
+ typeMapping.put(typeId, entryList)
+ oldTypeId = typeId
+ }
+ entryList.add(entryId)
+ }
+ return packageMapping
+}
+
+internal data class EntryWrapper(
+ val id: Int,
+ val packageName: String,
+ val type: String,
+ val entry: Resources.Entry
+)
+
+private fun toIdentifier(
+ resourcePackage: Resources.Package,
+ type: Resources.Type,
+ entry: Resources.Entry
+): Int =
+ (resourcePackage.packageId.id shl 24) or (type.typeId.id shl 16) or entry.entryId.id
+
+private fun packageIdFromIdentifier(
+ identifier: Int
+): Int =
+ identifier shr 24
+
+private fun typeIdFromIdentifier(
+ identifier: Int
+): Int =
+ (identifier and 0x00FF0000) shr 16
+
+private fun entryIdFromIdentifier(
+ identifier: Int
+): Int =
+ (identifier and 0x0000FFFF)
+
+
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
new file mode 100644
index 0000000..b592bb8
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ShrinkerDebugReporter.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker
+
+import java.io.File
+import java.io.PrintWriter
+
+interface ShrinkerDebugReporter : AutoCloseable {
+ fun debug(f: () -> String)
+ fun info(f: () -> String)
+}
+
+object NoDebugReporter : ShrinkerDebugReporter {
+ override fun debug(f: () -> String) = Unit
+
+ override fun info(f: () -> String) = Unit
+
+ override fun close() = Unit
+}
+
+class FileReporter(
+ reportFile: File
+) : ShrinkerDebugReporter {
+ private val writer: PrintWriter = reportFile.let { PrintWriter(it) }
+ override fun debug(f: () -> String) {
+ writer.println(f())
+ }
+
+ override fun info(f: () -> String) {
+ writer.println(f())
+ }
+
+ override fun close() {
+ writer.close()
+ }
+}
+
+class LoggerAndFileDebugReporter(
+ private val logDebug: (String) -> Unit,
+ private val logInfo: (String) -> Unit,
+ reportFile: File?
+) : ShrinkerDebugReporter {
+ private val writer: PrintWriter? = reportFile?.let { PrintWriter(it) }
+
+ override fun debug(f: () -> String) {
+ val message = f()
+ writer?.println(message)
+ logDebug(message)
+ }
+
+ override fun info(f: () -> String) {
+ val message = f()
+ writer?.println(message)
+ logInfo(message)
+ }
+
+ override fun close() {
+ writer?.close()
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt
new file mode 100644
index 0000000..c3c00eb
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ProtoResourceTableGatherer.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.gatherer
+
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.entriesSequence
+import com.android.ide.common.resources.resourceNameToFieldName
+import com.android.resources.ResourceType
+import java.nio.file.Path
+
+/**
+ * Gathers application resources from proto compiled resource table. Flattens each resource name to
+ * be compatible with field names in R classes.
+ *
+ * @param resourceTablePath path to resource table in proto format.
+ */
+class ProtoResourceTableGatherer(private val resourceTablePath: Path) : ResourcesGatherer {
+
+ override fun gatherResourceValues(model: ResourceShrinkerModel) {
+ model.readResourceTable(resourceTablePath).entriesSequence()
+ .forEach { (id, packageName, type, entry) ->
+ // We need to flatten resource names here to match fields names in R classes via
+ // invoking ResourcesUtil.resourceNameToFieldName because we need to record R fields
+ // usages in code.
+ ResourceType.fromClassName(type)
+ ?.takeIf { it != ResourceType.STYLEABLE }
+ ?.let {
+ model.addResource(it, packageName, resourceNameToFieldName(entry.name), id)
+ }
+ }
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java
new file mode 100644
index 0000000..17ce9cf
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/gatherer/ResourcesGatherer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.gatherer;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should gather application resources and contribute them to
+ * ResourceShrinkerModel
+ */
+public interface ResourcesGatherer {
+
+ /**
+ * Gathers application resources and contribute them to ResourceShrinkerModel via
+ * ResourceShrinkerModel.addResource
+ */
+ void gatherResourceValues(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
new file mode 100644
index 0000000..130e585
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.graph
+
+import com.android.aapt.Resources
+import com.android.aapt.Resources.Entry
+import com.android.aapt.Resources.FileReference
+import com.android.aapt.Resources.FileReference.Type.PROTO_XML
+import com.android.aapt.Resources.Reference
+import com.android.aapt.Resources.XmlAttribute
+import com.android.aapt.Resources.XmlElement
+import com.android.aapt.Resources.XmlNode
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.entriesSequence
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import com.android.ide.common.resources.usage.ResourceUsageModel.Resource
+import com.android.ide.common.resources.usage.WebTokenizers
+import com.android.resources.ResourceType
+import com.android.utils.SdkUtils.IMAGE_EXTENSIONS
+import com.google.common.base.Ascii
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Builds resources graph starting from each resource in resource table in proto format and follow
+ * all references to other resources inside inlined values and external files from res/ folder.
+ *
+ * <p>Supports external files in the following formats:
+ * <ul>
+ * <li>XML files compiled to proto format;
+ * <li>HTML, CSS, JS files inside res/raw folder;
+ * <li>Unknown files inside res/raw folder (only looks for 'android_res/<type>/<name>' pattern);
+ * </ul>
+ *
+ * <p>As ID resources don't specify parent-child relations between resources but are just
+ * identifiers for a resource or some part of the resource we don't gather them as references to
+ * examined resource.
+ *
+ * @param resourceRoot path to <module>/res/ folder.
+ * @param resourceTable path to resource table in proto format.
+ */
+class ProtoResourcesGraphBuilder(
+ private val resourceRoot: Path,
+ private val resourceTable: Path
+) : ResourcesGraphBuilder {
+
+ override fun buildGraph(model: ResourceShrinkerModel) {
+ model.readResourceTable(resourceTable).entriesSequence()
+ .map { (id, _, _, entry) ->
+ model.resourceStore.getResource(id)?.let {
+ ReferencesForResourceFinder(resourceRoot, model, entry, it)
+ }
+ }
+ .filterNotNull()
+ .forEach { it.findReferences() }
+ }
+}
+
+private class ReferencesForResourceFinder(
+ private val resourcesRoot: Path,
+ private val model: ResourceShrinkerModel,
+ private val entry: Entry,
+ private val current: Resource
+) {
+ companion object {
+ /**
+ * 'android_res/' is a synthetic directory for resource references in URL format. For
+ * example: file:///android_res/raw/intro_page.
+ */
+ private const val ANDROID_RES = "android_res/"
+
+ private const val CONSTRAINT_REFERENCED_IDS = "constraint_referenced_ids"
+
+ private fun Reference.asItem(): Resources.Item =
+ Resources.Item.newBuilder().setRef(this).build()
+ }
+
+ private val webTokenizers: WebTokenizers by lazy {
+ WebTokenizers(object : WebTokenizers.WebTokensCallback {
+ override fun referencedHtmlAttribute(tag: String?, attribute: String?, value: String) {
+ if (attribute == "href" || attribute == "src") {
+ referencedUrl(value)
+ }
+ }
+
+ override fun referencedJsString(jsString: String) {
+ referencedStringFromWebContent(jsString)
+ }
+
+ override fun referencedCssUrl(url: String) {
+ referencedUrl(url)
+ }
+
+ private fun referencedUrl(url: String) {
+ // 1. if url contains '/' try to find resources from this web url.
+ // 2. if there is no '/' it might be just relative reference to another resource.
+ val resources = when {
+ url.contains('/') -> model.resourceStore.getResourcesFromWebUrl(url)
+ else ->
+ model.resourceStore.getResources(ResourceType.RAW, url.substringBefore('.'))
+ }
+ if (resources.isNotEmpty()) {
+ resources.forEach { current.addReference(it) }
+ } else {
+ // if there is no resources found by provided url just gather this string as
+ // found inside web content to process it afterwards.
+ referencedStringFromWebContent(url)
+ }
+ }
+
+ private fun referencedStringFromWebContent(string: String) {
+ if (string.isNotEmpty() && string.length <= 80) {
+ model.addStringConstant(string)
+ model.isFoundWebContent = true
+ }
+ }
+ })
+ }
+
+ fun findReferences() {
+ // Walk through all values of the entry and find all Item instances that may reference
+ // other resources in resource table itself or specify external files that should be
+ // analyzed for references.
+ entry.configValueList.asSequence()
+ .map { it.value }
+ .flatMap { value ->
+ val compoundValue = value.compoundValue
+ // compoundValue.attr and compoundValue.styleable are skipped, attr defines
+ // references to ID resources only, but ID and STYLEABLE resources are not supported
+ // by shrinker.
+ when {
+ value.hasItem() ->
+ sequenceOf(value.item)
+ compoundValue.hasStyle() ->
+ sequenceOf(compoundValue.style.parent.asItem()) +
+ compoundValue.style.entryList.asSequence().flatMap {
+ sequenceOf(
+ it.item,
+ it.key.asItem()
+ )
+ }
+ compoundValue.hasArray() ->
+ compoundValue.array.elementList.asSequence().map { it.item }
+ compoundValue.hasPlural() ->
+ compoundValue.plural.entryList.asSequence().map { it.item }
+ else -> emptySequence()
+ }
+ }
+ .forEach { findFromItem(it) }
+ }
+
+ private fun findFromItem(item: Resources.Item) {
+ try {
+ when {
+ item.hasRef() -> findFromReference(item.ref)
+ item.hasFile() && item.file.path.startsWith("res/") -> findFromFile(item.file)
+ }
+ } catch (e: IOException) {
+ model.debugReporter.debug { "File '${item.file.path}' can not be processed. Skipping." }
+ }
+ }
+
+ private fun findFromReference(reference: Reference) {
+ // Reference object may have id of referenced resource, in this case prefer resolved id.
+ // In case id is not provided try to find referenced resource by name. Name is converted
+ // to resource url here, because name in resource table is not normalized to R style field
+ // and to find it we need normalize it first (for example, in case name in resource table is
+ // MyStyle.Child in R file it is R.style.MyStyle_child).
+ val referencedResources = when {
+ reference.id != 0 -> listOf(model.resourceStore.getResource(reference.id))
+ reference.name.isNotEmpty() ->
+ model.resourceStore.getResourcesFromUrl("@${reference.name}")
+ else -> emptyList()
+ }
+ // IDs are not supported by shrinker for now, just skip it.
+ referencedResources.asSequence()
+ .filterNotNull()
+ .filter { it.type != ResourceType.ID }
+ .forEach { current.addReference(it) }
+ }
+
+ private fun findFromFile(file: FileReference) {
+ val path = resourcesRoot.resolve(file.path.substringAfter("res/"))
+ val bytes: ByteArray by lazy { Files.readAllBytes(path) }
+ val content: String by lazy { String(bytes, StandardCharsets.UTF_8) }
+ val extension = Ascii.toLowerCase(path.fileName.toString()).substringAfter('.')
+ when {
+ file.type == PROTO_XML -> fillFromXmlNode(XmlNode.parseFrom(bytes))
+ extension in listOf("html", "htm") -> webTokenizers.tokenizeHtml(content)
+ extension == "css" -> webTokenizers.tokenizeCss(content)
+ extension == "js" -> webTokenizers.tokenizeJs(content)
+ extension !in IMAGE_EXTENSIONS -> maybeAndroidResUrl(content, markAsReachable = false)
+ }
+ }
+
+ private fun fillFromXmlNode(node: XmlNode) {
+ // Check for possible reference as 'android_res/<type>/<name>' pattern inside element text.
+ if (current.type == ResourceType.XML) {
+ maybeAndroidResUrl(node.text, markAsReachable = true)
+ }
+ // Check special xml element <rawPathResId> which provides reference to res/raw/ apk
+ // resource for wear application. Applies to all XML files for now but might be re-scoped
+ // to only apply to <wearableApp> XMLs.
+ maybeWearAppReference(node.element)
+ node.element.attributeList.forEach { fillFromAttribute(it) }
+ node.element.childList.forEach { fillFromXmlNode(it) }
+ }
+
+ private fun fillFromAttribute(attribute: XmlAttribute) {
+ if (attribute.name == CONSTRAINT_REFERENCED_IDS) {
+ fillFromConstraintReferencedIds(attribute.value)
+ }
+ if (attribute.hasCompiledItem()) {
+ findFromItem(attribute.compiledItem)
+ }
+ // Check for possible reference as 'android_res/<type>/<name>' pattern inside attribute val.
+ if (current.type == ResourceType.XML) {
+ maybeAndroidResUrl(attribute.value, markAsReachable = true)
+ }
+ }
+
+ private fun fillFromConstraintReferencedIds(value: String?) {
+ value
+ ?.split(",")
+ ?.map { it.trim() }
+ ?.forEach {
+ model.resourceStore.getResources(ResourceType.ID, it)
+ .forEach(ResourceUsageModel::markReachable)
+ }
+ }
+
+ private fun maybeAndroidResUrl(text: String, markAsReachable: Boolean) {
+ findAndroidResReferencesInText(text)
+ .map { it.split('/', limit = 2) }
+ .filter { it.size == 2 }
+ .map { (dir, fileName) ->
+ Pair(
+ ResourceType.fromFolderName(dir.substringBefore('-')),
+ fileName.substringBefore('.')
+ )
+ }
+ .filter { (type, _) -> type != null }
+ .flatMap { (type, name) -> model.resourceStore.getResources(type!!, name).asSequence() }
+ .forEach {
+ if (markAsReachable) {
+ ResourceUsageModel.markReachable(it)
+ } else {
+ current.addReference(it)
+ }
+ }
+ }
+
+ private fun maybeWearAppReference(element: XmlElement) {
+ if (element.name == "rawPathResId") {
+ val rawResourceName = element.childList
+ .map { it.text }
+ .joinToString(separator = "")
+ .trim()
+
+ model.resourceStore.getResources(ResourceType.RAW, rawResourceName)
+ .forEach { current.addReference(it) }
+ }
+ }
+
+ /**
+ * Splits input text to parts that starts with 'android_res/' and returns sequence of strings
+ * between 'android_res/' occurrences and first whitespace after it. This method is used instead
+ * of {@link CharSequence#splitToSequence} because does not spawn full substrings between
+ * 'android_res/' in memory when text is big enough.
+ */
+ private fun findAndroidResReferencesInText(text: String): Sequence<String> = sequence {
+ var start = 0
+ while (start < text.length) {
+ start = text.indexOf(ANDROID_RES, start)
+ if (start == -1) {
+ break
+ }
+ var end = start + ANDROID_RES.length
+ while (end < text.length && !Character.isWhitespace(text[end])) {
+ end++
+ }
+ yield(text.substring(start + ANDROID_RES.length, end))
+ start = end
+ }
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java b/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java
new file mode 100644
index 0000000..21feb52
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ResourcesGraphBuilder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.graph;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should find references between resources which are gathered inside
+ * ResourceShrinkerModel.
+ */
+public interface ResourcesGraphBuilder {
+
+ /**
+ * Finds references between resources and connects them. May introduce and contribute new
+ * resources to ResourceShrinkerModel.
+ */
+ void buildGraph(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt
new file mode 100644
index 0000000..3cf757d
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscatedClasses.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.obfuscation
+
+import com.google.common.collect.ImmutableMap
+
+/** Contains mappings between obfuscated classes and methods to original ones. */
+class ObfuscatedClasses private constructor(builder: Builder) {
+
+ companion object {
+ @JvmField
+ val NO_OBFUSCATION = Builder().build()
+ }
+
+ private val obfuscatedClasses = ImmutableMap.copyOf(builder.obfuscatedClasses)
+ private val obfuscatedMethods = ImmutableMap.copyOf(builder.obfuscatedMethods)
+
+ fun resolveOriginalMethod(obfuscatedMethod: ClassAndMethod): ClassAndMethod {
+ return obfuscatedMethods.getOrElse(obfuscatedMethod) {
+ val realClassName = obfuscatedClasses[obfuscatedMethod.className] ?: obfuscatedMethod.className
+ ClassAndMethod(realClassName, obfuscatedMethod.methodName)
+ }
+ }
+
+ fun resolveOriginalClass(obfuscatedClass: String): String {
+ return obfuscatedClasses[obfuscatedClass] ?: obfuscatedClass
+ }
+
+ /**
+ * Builder that allows to build obfuscated mappings in a way when next method mapping is added
+ * to previous class mapping. Example:
+ * builder
+ * .addClassMapping(Pair(classA, obfuscatedClassA))
+ * .addMethodMapping(Pair(classAMethod1, obfuscatedClassAMethod1))
+ * .addMethodMapping(Pair(classAMethod2, obfuscatedClassAMethod2))
+ * .addClassMapping(Pair(classB, obfuscatedClassB))
+ * .addMethodMapping(Pair(classBMethod1, obfuscatedClassBMethod1))
+ */
+ class Builder {
+
+ val obfuscatedClasses: MutableMap<String, String> = mutableMapOf()
+ val obfuscatedMethods: MutableMap<ClassAndMethod, ClassAndMethod> = mutableMapOf()
+
+ var currentClassMapping: Pair<String, String>? = null
+
+ /**
+ * Adds class mapping: original class name -> obfuscated class name.
+ *
+ * @param mapping Pair(originalClassName, obfuscatedClassName)
+ */
+ fun addClassMapping(mapping: Pair<String, String>): Builder {
+ currentClassMapping = mapping
+ obfuscatedClasses += Pair(mapping.second, mapping.first)
+ return this
+ }
+
+ /**
+ * Adds method mapping: original method name -> obfuscated method name to the latest added
+ * class mapping.
+ *
+ * @param mapping Pair(originalMethodName, obfuscatedMethodName)
+ */
+ fun addMethodMapping(mapping: Pair<String, String>): Builder {
+ if (currentClassMapping != null) {
+ obfuscatedMethods += Pair(
+ ClassAndMethod(currentClassMapping!!.second, mapping.second),
+ ClassAndMethod(currentClassMapping!!.first, mapping.first)
+ )
+ }
+ return this
+ }
+
+ fun build(): ObfuscatedClasses =
+ ObfuscatedClasses(this)
+ }
+}
+
+data class ClassAndMethod(val className: String, val methodName: String)
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java
new file mode 100644
index 0000000..6e76c8d
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ObfuscationMappingsRecorder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.obfuscation;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should record obfuscation mappings and contribute it to
+ * ResourceShrinkerModel.obfuscatedClasses.
+ */
+public interface ObfuscationMappingsRecorder {
+
+ /**
+ * Records obfuscation mappings to be able to resolve original class name and original method
+ * name when all references in code are obfuscated. Should create and contribute
+ * ObfuscatedClasses instance to ResourceShrinkerModel.obfuscatedClasses.
+ */
+ void recordObfuscationMappings(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
new file mode 100644
index 0000000..dd538c7
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.obfuscation
+
+import com.android.build.shrinker.ResourceShrinkerModel
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records obfuscation mappings from single file in proguard format.
+ *
+ * @param mappingsFile path to proguard.map file.
+ */
+class ProguardMappingsRecorder(private val mappingsFile: Path) : ObfuscationMappingsRecorder {
+
+ override fun recordObfuscationMappings(model: ResourceShrinkerModel) {
+ model.obfuscatedClasses = extractObfuscatedResourceClasses()
+ }
+
+ internal fun extractObfuscatedResourceClasses(): ObfuscatedClasses {
+ // Proguard obfuscation mappings file has the following structure:
+ // # comment1
+ // com.package.MainClass -> a.a:
+ // int field -> a
+ // boolean getFlag() -> b
+ // com.package.R -> a.b:
+ // com.package.R$style -> a.b.a:
+ // int Toolbar_android_gravity -> i1
+
+ val builder = ObfuscatedClasses.Builder()
+ Files.readAllLines(mappingsFile, StandardCharsets.UTF_8).forEach { line ->
+ when {
+ isMethodMapping(line) -> builder.addMethodMapping(extractMethodMapping(line))
+ isClassMapping(line) -> builder.addClassMapping(extractClassMapping(line))
+ }
+ }
+ return builder.build()
+ }
+
+ private fun isClassMapping(line: String): Boolean = line.contains("->")
+
+ private fun isMethodMapping(line: String): Boolean =
+ (line.startsWith(" ") || line.startsWith("\t")) && line.contains("->")
+
+ private fun extractClassMapping(line: String): Pair<String, String> {
+ val mapping = line.split("->", limit = 2)
+ return Pair(mapping[0].trim(), mapping[1].trim(' ', '\t', ':'))
+ }
+
+ private fun extractMethodMapping(line: String): Pair<String, String> {
+ val mapping = line.split("->", limit = 2)
+ val originalMethod = mapping[0].trim()
+ .substringBeforeLast('(')
+ .substringAfter(' ')
+ val obfuscatedMethod = mapping[1].trim()
+ return Pair(originalMethod, obfuscatedMethod)
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt
new file mode 100644
index 0000000..bbfea7c
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/AppCompat.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.build.shrinker.obfuscation.ObfuscatedClasses
+
+internal object AppCompat {
+ // Known AppCompat classes which use Resources.getIdentifier but should not trigger mode that
+ // tries to find used resources based on collected strings.
+ internal val APP_COMPAT_CLASSES_ALLOWED_FOR_GET_IDENTIFIER = setOf(
+ "android.support.v7.widget.SuggestionsAdapter",
+ "android.support.v7.internal.widget.ResourcesWrapper",
+ "android.support.v7.widget.ResourcesWrapper",
+ "android.support.v7.widget.TintContextWrapper\$TintResources"
+ )
+
+ internal fun isAppCompatClass(name: String, obfuscation: ObfuscatedClasses): Boolean =
+ APP_COMPAT_CLASSES_ALLOWED_FOR_GET_IDENTIFIER.contains(obfuscation.resolveOriginalClass(name))
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
new file mode 100644
index 0000000..3f1a676
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.SdkConstants.DOT_DEX
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.build.shrinker.obfuscation.ClassAndMethod
+import com.android.build.shrinker.usages.AppCompat.isAppCompatClass
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import com.android.resources.ResourceType
+import com.android.tools.r8.references.MethodReference
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records resource usages, detects usages of WebViews and {@code Resources#getIdentifier},
+ * gathers string constants from compiled .dex files.
+ *
+ * @param root directory starting from which all .dex files are analyzed.
+ */
+class DexUsageRecorder(val root: Path) : ResourceUsageRecorder {
+
+ override fun recordUsages(model: ResourceShrinkerModel) {
+ // Record resource usages from dex classes. The following cases are covered:
+ // 1. Integer constant which refers to resource id.
+ // 2. Reference to static field in R classes.
+ // 3. Usages of android.content.res.Resources.getIdentifier(...) and
+ // android.webkit.WebView.load...
+ // 4. All strings which might be used to reference resources by name via
+ // Resources.getIdentifier.
+
+ Files.walk(root)
+ .filter { Files.isRegularFile(it) }
+ .filter { it.toString().endsWith(DOT_DEX, ignoreCase = true) }
+ .forEach { path ->
+ runResourceShrinkerAnalysis(
+ Files.readAllBytes(path),
+ path,
+ DexFileAnalysisCallback(path, model)
+ )
+ }
+ }
+}
+
+private class DexFileAnalysisCallback(
+ private val path: Path,
+ private val model: ResourceShrinkerModel
+) : AnalysisCallback {
+ companion object {
+ const val ANDROID_RES = "android_res/"
+
+ private fun String.toSourceClassName(): String {
+ return this.replace('/', '.')
+ }
+ }
+
+ // R class methods should only be processed for reachable resource IDs. R class fields that are
+ // not referenced should not be considered since there is no usage in the program.
+ // If the fields have been inlined, the values at the callsite will be recorded when visited.
+ var isRClass: Boolean = false
+
+ // In cases where a value from a method is inlined into a constant, we should still mark the
+ // resource as used.
+ val visitingMethod = MethodVisitingStatus()
+
+ override fun shouldProcess(internalName: String): Boolean {
+ isRClass = isResourceClass(internalName)
+ return true
+ }
+
+ /** Returns whether the given class file name points to an aapt-generated compiled R class. */
+ fun isResourceClass(internalName: String): Boolean {
+ val realClassName =
+ model.obfuscatedClasses.resolveOriginalClass(internalName.toSourceClassName())
+ val lastPart = realClassName.substringAfterLast('.')
+ if (lastPart.startsWith("R$")) {
+ val typeName = lastPart.substring(2)
+ return ResourceType.fromClassName(typeName) != null
+ }
+ return false
+ }
+
+ override fun referencedInt(value: Int) {
+ // Avoid marking R class fields as reachable.
+ if (shouldIgnoreField()) {
+ return
+ }
+ val resource = model.resourceStore.getResource(value)
+ if (ResourceUsageModel.markReachable(resource)) {
+ model.debugReporter.debug {
+ "Marking $resource reachable: referenced from $path"
+ }
+ }
+ }
+
+ override fun referencedStaticField(internalName: String, fieldName: String) {
+ // Avoid marking R class fields as reachable.
+ if (shouldIgnoreField()) {
+ return
+ }
+ val realMethod = model.obfuscatedClasses.resolveOriginalMethod(
+ ClassAndMethod(internalName.toSourceClassName(), fieldName)
+ )
+
+ if (isValidResourceType(realMethod.className)) {
+ val typePart = realMethod.className.substringAfterLast('$')
+ ResourceType.fromClassName(typePart)?.let { type ->
+ model.resourceStore.getResources(type, realMethod.methodName)
+ .forEach { ResourceUsageModel.markReachable(it) }
+ }
+ }
+ }
+
+ override fun referencedString(value: String) {
+ // Avoid marking R class fields as reachable.
+ if (shouldIgnoreField()) {
+ return
+ }
+ // See if the string is at all eligible; ignore strings that aren't identifiers (has java
+ // identifier chars and nothing but .:/), or are empty or too long.
+ // We also allow "%", used for formatting strings.
+ if (value.isEmpty() || value.length > 80) {
+ return
+ }
+ fun isSpecialCharacter(c: Char) = c == '.' || c == ':' || c == '/' || c == '%'
+
+ if (value.all { Character.isJavaIdentifierPart(it) || isSpecialCharacter(it) } &&
+ value.any { Character.isJavaIdentifierPart(it) }) {
+ model.addStringConstant(value)
+ model.isFoundWebContent = model.isFoundWebContent || value.contains(ANDROID_RES)
+ }
+ }
+
+ override fun referencedMethod(
+ internalName: String,
+ methodName: String,
+ methodDescriptor: String
+ ) {
+ if (isRClass && visitingMethod.isVisiting && visitingMethod.methodName == "<clinit>") {
+ return
+ }
+ if (internalName == "android/content/res/Resources" &&
+ methodName == "getIdentifier" &&
+ methodDescriptor == "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I"
+ ) {
+ // "benign" usages: don't trigger reflection mode just because the user has included
+ // appcompat
+ if (isAppCompatClass(internalName.toSourceClassName(), model.obfuscatedClasses)) {
+ return
+ }
+ model.isFoundGetIdentifier = true
+ // TODO: Check previous instruction and see if we can find a literal String; if so, we
+ // can more accurately dispatch the resource here rather than having to check the whole
+ // string pool!
+ }
+ if (internalName == "android/webkit/WebView" && methodName.startsWith("load")) {
+ model.isFoundWebContent = true
+ }
+ }
+
+ override fun startMethodVisit(methodReference: MethodReference) {
+ visitingMethod.isVisiting = true
+ visitingMethod.methodName = methodReference.methodName
+ }
+
+ override fun endMethodVisit(methodReference: MethodReference) {
+ visitingMethod.isVisiting = false
+ visitingMethod.methodName = null
+ }
+
+ private fun shouldIgnoreField(): Boolean {
+ val visitingFromStaticInitRClass = (isRClass
+ && visitingMethod.isVisiting
+ && (visitingMethod.methodName == "<clinit>"))
+ return visitingFromStaticInitRClass ||
+ isRClass && !visitingMethod.isVisiting
+ }
+
+ private fun isValidResourceType(className: String): Boolean =
+ className.substringAfterLast('.').startsWith("R$")
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
new file mode 100644
index 0000000..4e6aa3e
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.aapt.Resources.XmlNode
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.ide.common.resources.usage.ResourceUsageModel
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Records resource usages from AndroidManifest.xml in proto compiled format.
+ *
+ * @param manifest path to AndroidManifest.xml file.
+ */
+class ProtoAndroidManifestUsageRecorder(private val manifest: Path) : ResourceUsageRecorder {
+
+ override fun recordUsages(model: ResourceShrinkerModel) {
+ val root = XmlNode.parseFrom(Files.readAllBytes(manifest))
+ recordUsagesFromNode(root, model)
+ }
+
+ private fun recordUsagesFromNode(node: XmlNode, model: ResourceShrinkerModel) {
+ // Records only resources from element attributes that have reference items with resolved
+ // ids or names.
+ if (!node.hasElement()) {
+ return
+ }
+ node.element.attributeList.asSequence()
+ .filter { it.hasCompiledItem() }
+ .map { it.compiledItem }
+ .filter { it.hasRef() }
+ .map { it.ref }
+ .flatMap {
+ // If resource id is available prefer this id to name.
+ when {
+ it.id != 0 -> listOfNotNull(model.resourceStore.getResource(it.id))
+ else -> model.resourceStore.getResourcesFromUrl("@${it.name}")
+ }.asSequence()
+ }
+ .forEach { ResourceUsageModel.markReachable(it) }
+ node.element.childList.forEach { recordUsagesFromNode(it, model) }
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java b/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java
new file mode 100644
index 0000000..e32f8f6
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ResourceUsageRecorder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.usages;
+
+import com.android.annotations.NonNull;
+import com.android.build.shrinker.ResourceShrinkerModel;
+import java.io.IOException;
+
+/**
+ * Interface for unit that should record resource usages and contribute this information to
+ * ResourceShrinkerModel.
+ */
+public interface ResourceUsageRecorder {
+
+ /** Records resource usages. */
+ void recordUsages(@NonNull ResourceShrinkerModel model) throws IOException;
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
new file mode 100644
index 0000000..3d0dc6f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.android.build.shrinker.usages
+
+import com.android.SdkConstants.VALUE_STRICT
+import com.android.build.shrinker.ResourceShrinkerModel
+import com.android.utils.XmlUtils
+import com.google.common.collect.ImmutableMap.copyOf
+import java.io.Reader
+import java.nio.file.Files
+import java.nio.file.Path
+import javax.xml.stream.XMLInputFactory
+
+/**
+ * Records usages of tools:keep, tools:discard and tools:shrinkMode in resources.
+ *
+ * <p>This unit requires to analyze resources in raw XML format because as said in
+ * <a href="https://developer.android.com/studio/write/tool-attributes>documentation</a> these
+ * attributes may appear in any <resources> element and some files that contain such element
+ * are not compiled to proto. For example raw and values resources like res/raw/keep.xml,
+ * res/values/values.xml etc.
+ *
+ * @param rawResourcesPath path to folder with resources in raw format.
+ */
+class ToolsAttributeUsageRecorder(val rawResourcesPath: Path) : ResourceUsageRecorder {
+ companion object {
+ private val TOOLS_NAMESPACE = "http://schemas.android.com/tools"
+ }
+
+ override fun recordUsages(model: ResourceShrinkerModel) {
+ Files.walk(rawResourcesPath)
+ .filter { it.fileName.toString().endsWith(".xml", ignoreCase = true) }
+ .forEach { processRawXml(it, model) }
+ }
+
+ private fun processRawXml(path: Path, model: ResourceShrinkerModel) {
+ processResourceToolsAttributes(path).forEach { key, value ->
+ when (key) {
+ "keep" -> model.resourceStore.recordKeepToolAttribute(value)
+ "discard" -> model.resourceStore.recordDiscardToolAttribute(value)
+ "shrinkMode" ->
+ if (value == VALUE_STRICT) {
+ model.resourceStore.safeMode = false
+ }
+ }
+ }
+ }
+
+ private fun processResourceToolsAttributes(path: Path): Map<String, String> {
+ val toolsAttributes = mutableMapOf<String, String>()
+ XmlUtils.getUtfReader(path).use { reader: Reader ->
+ val factory = XMLInputFactory.newInstance()
+ val xmlStreamReader = factory.createXMLStreamReader(reader)
+
+ var rootElementProcessed = false
+ while (!rootElementProcessed && xmlStreamReader.hasNext()) {
+ xmlStreamReader.next()
+ if (xmlStreamReader.isStartElement) {
+ if (xmlStreamReader.localName == "resources") {
+ for (i in 0 until xmlStreamReader.attributeCount) {
+ if (xmlStreamReader.getAttributeNamespace(i) == TOOLS_NAMESPACE) {
+ toolsAttributes.put(
+ xmlStreamReader.getAttributeLocalName(i),
+ xmlStreamReader.getAttributeValue(i)
+ )
+ }
+ }
+ }
+ rootElementProcessed = true
+ }
+ }
+ }
+ return copyOf(toolsAttributes)
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt
new file mode 100644
index 0000000..6bfd90f
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/r8ResourceShrinker.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+@file:JvmName("R8ResourceShrinker")
+
+package com.android.build.shrinker.usages
+
+import com.android.tools.r8.ProgramResource
+import com.android.tools.r8.ProgramResourceProvider
+import com.android.tools.r8.ResourceShrinker
+import com.android.tools.r8.origin.PathOrigin
+import com.android.tools.r8.references.MethodReference
+import java.nio.file.Path
+
+
+fun runResourceShrinkerAnalysis(bytes: ByteArray, file: Path, callback: AnalysisCallback) {
+ val resource =
+ ProgramResource.fromBytes(PathOrigin(file), ProgramResource.Kind.DEX, bytes, null)
+ val provider = ProgramResourceProvider { listOf(resource) }
+
+ val command = ResourceShrinker.Builder().addProgramResourceProvider(provider).build()
+ ResourceShrinker.run(command, AnalysisAdapter(callback))
+}
+
+/** An adapter so R8 API classes do not leak into other modules. */
+class AnalysisAdapter(val impl: AnalysisCallback) : ResourceShrinker.ReferenceChecker {
+ override fun shouldProcess(internalName: String): Boolean = impl.shouldProcess(internalName)
+
+ override fun referencedStaticField(internalName: String, fieldName: String) =
+ impl.referencedStaticField(internalName, fieldName)
+
+ override fun referencedInt(value: Int) = impl.referencedInt(value)
+
+ override fun referencedString(value: String) = impl.referencedString(value)
+
+ override fun referencedMethod(
+ internalName: String, methodName: String, methodDescriptor: String
+ ) = impl.referencedMethod(internalName, methodName, methodDescriptor)
+
+ override fun startMethodVisit(methodReference: MethodReference
+ ) = impl.startMethodVisit(methodReference)
+
+ override fun endMethodVisit(methodReference: MethodReference
+ ) = impl.endMethodVisit(methodReference)
+}
+
+interface AnalysisCallback {
+
+ fun shouldProcess(internalName: String): Boolean
+
+ fun referencedInt(value: Int)
+
+ fun referencedString(value: String)
+
+ fun referencedStaticField(internalName: String, fieldName: String)
+
+ fun referencedMethod(internalName: String, methodName: String, methodDescriptor: String)
+
+ fun startMethodVisit(methodReference: MethodReference)
+
+ fun endMethodVisit(methodReference: MethodReference)
+}
+
+class MethodVisitingStatus(var isVisiting: Boolean = false, var methodName: String? = null)
diff --git a/src/test/examples/shaking1/print-mapping-dex.ref b/src/test/examples/shaking1/print-mapping-dex.ref
index bb72965..cd0d3a3 100644
--- a/src/test/examples/shaking1/print-mapping-dex.ref
+++ b/src/test/examples/shaking1/print-mapping-dex.ref
@@ -1,9 +1,8 @@
shaking1.Shaking -> shaking1.Shaking:
+ 0:8:void main(java.lang.String[]):8:8 -> main
+ 9:21:void main(java.lang.String[]):9:9 -> main
shaking1.Used -> a.a:
- 0:2:void <init>(java.lang.String):12:12 -> <init>
- 0:2:java.lang.String aMethodThatIsNotUsedButKept():0:0 -> aMethodThatIsNotUsedButKept
- 3:5:void <init>(java.lang.String):13:13 -> <init>
- 0:6:void main(java.lang.String[]):8:8 -> main
- 7:21:void main(java.lang.String[]):9:9 -> main
- 0:16:java.lang.String method():17:17 -> a
java.lang.String name -> a
+ 0:5:void <init>(java.lang.String):12:12 -> <init>
+ 0:16:java.lang.String method():17:17 -> a
+ 0:2:java.lang.String aMethodThatIsNotUsedButKept():0:0 -> aMethodThatIsNotUsedButKept
diff --git a/src/test/examplesJava17/records/RecordWithAnnotations.java b/src/test/examplesJava17/records/RecordWithAnnotations.java
index d1648b6..fe9fd95 100644
--- a/src/test/examplesJava17/records/RecordWithAnnotations.java
+++ b/src/test/examplesJava17/records/RecordWithAnnotations.java
@@ -10,6 +10,9 @@
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
public class RecordWithAnnotations {
@@ -56,16 +59,39 @@
System.out.println(c.getType().getName());
System.out.println(c.getGenericSignature() == null);
System.out.println(c.getAnnotations().length);
+ // Collect and sort the annotations, as the order is not deterministic on Art (tested
+ // on Art 14 Beta 3).
+ List<String> annotations = new ArrayList<>();
for (int j = 0; j < c.getAnnotations().length; j++) {
- System.out.println(c.getAnnotations()[j]);
+ annotations.add(c.getAnnotations()[j].toString());
+ }
+ annotations.sort(Comparator.naturalOrder());
+ for (int j = 0; j < annotations.size(); j++) {
+ System.out.println(annotations.get(j));
}
}
System.out.println(Person.class.getDeclaredFields().length);
+ List<Field> fields = new ArrayList<>();
for (int i = 0; i < Person.class.getDeclaredFields().length; i++) {
- Field f = Person.class.getDeclaredFields()[i];
+ fields.add(Person.class.getDeclaredFields()[i]);
+ }
+ fields.sort(
+ new Comparator<Field>() {
+ @Override
+ public int compare(Field o1, Field o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ });
+ for (int i = 0; i < fields.size(); i++) {
+ Field f = fields.get(i);
System.out.println(f.getDeclaredAnnotations().length);
+ List<String> annotations = new ArrayList<>();
for (int j = 0; j < f.getDeclaredAnnotations().length; j++) {
- System.out.println(f.getAnnotations()[j]);
+ annotations.add(f.getAnnotations()[j].toString());
+ }
+ annotations.sort(Comparator.naturalOrder());
+ for (int j = 0; j < annotations.size(); j++) {
+ System.out.println(annotations.get(j));
}
}
}
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 28625eb..595b3b7 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -168,6 +168,11 @@
return assertFailure();
}
+ public RR assertFailureWithOutputThatMatches(Matcher<String> matcher) {
+ assertStdoutMatches(matcher);
+ return assertFailure();
+ }
+
public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
return assertFailureWithErrorThatMatches(containsString(expectedError.getName()));
}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 1151357..596a13d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -789,7 +789,7 @@
ImmutableMap.<DexVm, String>builder()
.put(DexVm.ART_DEFAULT, "art")
.put(DexVm.ART_MASTER_HOST, "host/art-master")
- .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-dp1")
+ .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-beta3")
.put(DexVm.ART_13_0_0_HOST, "host/art-13.0.0")
.put(DexVm.ART_12_0_0_HOST, "host/art-12.0.0-beta4")
.put(DexVm.ART_10_0_0_HOST, "art-10.0.0")
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
index cda9c15..8605db0 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
@@ -110,9 +110,9 @@
methodReferences.forEach(field -> numberOfMethods.increment())));
});
// These numbers will change when updating api-versions.xml
- assertEquals(5635, parsedApiClasses.size());
- assertEquals(29017, numberOfFields.get());
- assertEquals(44107, numberOfMethods.get());
+ assertEquals(5716, parsedApiClasses.size());
+ assertEquals(29609, numberOfFields.get());
+ assertEquals(44827, numberOfMethods.get());
}
@Test
diff --git a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
index 9095f727..3f12fc8 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/L8Benchmark.java
@@ -3,23 +3,19 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.benchmarks.desugaredlib;
-import static com.android.tools.r8.ToolHelper.getDesugarLibConversions;
-import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.CustomConversionVersion.LATEST;
-
import com.android.tools.r8.L8TestBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestState;
+import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.benchmarks.BenchmarkBase;
import com.android.tools.r8.benchmarks.BenchmarkConfig;
import com.android.tools.r8.benchmarks.BenchmarkDependency;
import com.android.tools.r8.benchmarks.BenchmarkEnvironment;
import com.android.tools.r8.benchmarks.BenchmarkTarget;
-import com.android.tools.r8.desugar.desugaredlibrary.jdk11.DesugaredLibraryJDK11Undesugarer;
import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.junit.runner.RunWith;
@@ -29,10 +25,12 @@
@RunWith(Parameterized.class)
public class L8Benchmark extends BenchmarkBase {
- private static final BenchmarkDependency androidJar = BenchmarkDependency.getAndroidJar30();
- private static final BenchmarkDependency jdk11Conf =
+ private static final BenchmarkDependency ANDROID_JAR = BenchmarkDependency.getAndroidJar30();
+ private static final BenchmarkDependency LEGACY_CONF =
new BenchmarkDependency(
- "legacyConf", "desugar_jdk_libs_11", Paths.get("third_party", "openjdk"));
+ "legacyConf",
+ "2.0.3",
+ Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "desugar_jdk_libs_releases"));
public L8Benchmark(BenchmarkConfig config, TestParameters parameters) {
super(config, parameters);
@@ -50,23 +48,21 @@
.setTarget(BenchmarkTarget.D8)
.setFromRevision(12733)
.setMethod(L8Benchmark::run)
- .addDependency(androidJar)
- .addDependency(jdk11Conf)
+ .addDependency(ANDROID_JAR)
+ .addDependency(LEGACY_CONF)
.measureRunTime()
.build());
}
public static void run(BenchmarkEnvironment environment) throws Exception {
- Path undesugarJdkLib =
- DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
- environment.getTemp().newFolder("undesugar_jdk_lib").toPath(),
- jdk11Conf.getRoot(environment).resolve("desugar_jdk_libs.jar"));
LibraryDesugaringSpecification spec =
new LibraryDesugaringSpecification(
"JDK11_Benchmark",
- ImmutableSet.of(undesugarJdkLib, getDesugarLibConversions(LATEST)),
- Paths.get("src/library_desugar/jdk11/desugar_jdk_libs.json"),
- ImmutableSet.of(androidJar.getRoot(environment).resolve("android.jar")),
+ ImmutableSet.of(
+ LEGACY_CONF.getRoot(environment).resolve("desugar_jdk_libs.jar"),
+ LEGACY_CONF.getRoot(environment).resolve("desugar_jdk_libs_configuration.jar")),
+ LEGACY_CONF.getRoot(environment).resolve("desugar.json"),
+ ImmutableSet.of(ANDROID_JAR.getRoot(environment).resolve("android.jar")),
LibraryDesugaringSpecification.JDK11_DESCRIPTOR,
"");
runner(environment.getConfig())
diff --git a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
index 0e86477..6b5e4f9 100644
--- a/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
+++ b/src/test/java/com/android/tools/r8/benchmarks/desugaredlib/LegacyDesugaredLibraryBenchmark.java
@@ -24,12 +24,12 @@
@RunWith(Parameterized.class)
public class LegacyDesugaredLibraryBenchmark extends BenchmarkBase {
- private static final BenchmarkDependency androidJar = BenchmarkDependency.getAndroidJar30();
- private static final BenchmarkDependency legacyConf =
+ private static final BenchmarkDependency ANDROID_JAR = BenchmarkDependency.getAndroidJar30();
+ private static final BenchmarkDependency LEGACY_CONF =
new BenchmarkDependency(
"legacyConf",
- "desugar_jdk_libs_legacy",
- Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk"));
+ "1.1.5",
+ Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "desugar_jdk_libs_releases"));
public LegacyDesugaredLibraryBenchmark(BenchmarkConfig config, TestParameters parameters) {
super(config, parameters);
@@ -47,8 +47,8 @@
.setTarget(BenchmarkTarget.D8)
.setFromRevision(12150)
.setMethod(LegacyDesugaredLibraryBenchmark::run)
- .addDependency(androidJar)
- .addDependency(legacyConf)
+ .addDependency(ANDROID_JAR)
+ .addDependency(LEGACY_CONF)
.measureRunTime()
.build());
}
@@ -62,15 +62,13 @@
results ->
testForD8(environment.getTemp(), Backend.DEX)
.setMinApi(AndroidApiLevel.B)
- .addLibraryFiles(androidJar.getRoot(environment).resolve("android.jar"))
+ .addLibraryFiles(ANDROID_JAR.getRoot(environment).resolve("android.jar"))
.apply(
b ->
b.getBuilder()
.addDesugaredLibraryConfiguration(
StringResource.fromFile(
- legacyConf
- .getRoot(environment)
- .resolve("desugar_jdk_libs.json"))))
+ LEGACY_CONF.getRoot(environment).resolve("desugar.json"))))
.benchmarkCompile(results));
}
diff --git a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
index 802ccce..fbbadb0 100644
--- a/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
+++ b/src/test/java/com/android/tools/r8/cf/methodhandles/invokespecial/InvokeSpecialMethodHandleTest.java
@@ -130,7 +130,12 @@
} else if (parameters.isDexRuntime()
&& parameters.asDexRuntime().getVersion().isNewerThan(Version.V9_0_0)) {
// VMs between 9 and 13 segfault.
- result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+ if (parameters.asDexRuntime().getVersion().isEqualTo(Version.V14_0_0)) {
+ result.assertFailureWithOutputThatMatches(
+ containsString("reverting to SIG_DFL handler for signal 11"));
+ } else {
+ result.assertFailureWithErrorThatMatches(containsString("HandleUnexpectedSignal"));
+ }
} else {
result.assertSuccessWithOutput(getExpected());
}
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java
new file mode 100644
index 0000000..ab69d27
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/InstructionTypeMapper.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cfmethodgeneration;
+
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import java.util.Map;
+import java.util.function.Function;
+
+public class InstructionTypeMapper {
+
+ private final DexItemFactory factory;
+ private final Map<DexType, DexType> typeMap;
+ private final Function<String, String> methodNameMap;
+
+ public InstructionTypeMapper(
+ DexItemFactory factory,
+ Map<DexType, DexType> typeMap,
+ Function<String, String> methodNameMap) {
+ this.factory = factory;
+ this.typeMap = typeMap;
+ this.methodNameMap = methodNameMap;
+ }
+
+ public CfInstruction rewriteInstruction(CfInstruction instruction) {
+ if (instruction.isTypeInstruction()) {
+ CfInstruction rewritten = rewriteTypeInstruction(instruction.asTypeInstruction());
+ return rewritten == null ? instruction : rewritten;
+ }
+ if (instruction.isFieldInstruction()) {
+ return rewriteFieldInstruction(instruction.asFieldInstruction());
+ }
+ if (instruction.isInvoke()) {
+ return rewriteInvokeInstruction(instruction.asInvoke());
+ }
+ if (instruction.isFrame()) {
+ return rewriteFrameInstruction(instruction.asFrame());
+ }
+ return instruction;
+ }
+
+ private CfInstruction rewriteInvokeInstruction(CfInvoke instruction) {
+ CfInvoke invoke = instruction.asInvoke();
+ DexMethod method = invoke.getMethod();
+ String name = method.getName().toString();
+ DexType holderType = invoke.getMethod().getHolderType();
+ DexType rewrittenType = typeMap.getOrDefault(holderType, holderType);
+ String rewrittenName =
+ rewrittenType == factory.varHandleType ? methodNameMap.apply(name) : name;
+ if (rewrittenType != holderType) {
+ return new CfInvoke(
+ invoke.getOpcode(),
+ factory.createMethod(
+ rewrittenType,
+ rewriteProto(invoke.getMethod().getProto()),
+ factory.createString(rewrittenName)),
+ invoke.isInterface());
+ }
+ return instruction;
+ }
+
+ private DexProto rewriteProto(DexProto proto) {
+ return factory.createProto(
+ typeMap.getOrDefault(proto.returnType, proto.returnType),
+ proto.parameters.stream()
+ .map(type -> typeMap.getOrDefault(type, type))
+ .toArray(DexType[]::new));
+ }
+
+ private CfFieldInstruction rewriteFieldInstruction(CfFieldInstruction instruction) {
+ DexType holderType = instruction.getField().getHolderType();
+ DexType rewrittenHolderType = typeMap.getOrDefault(holderType, holderType);
+ DexType fieldType = instruction.getField().getType();
+ DexType rewrittenType = typeMap.getOrDefault(fieldType, fieldType);
+ if (rewrittenHolderType != holderType || rewrittenType != fieldType) {
+ return instruction.createWithField(
+ factory.createField(rewrittenHolderType, rewrittenType, instruction.getField().name));
+ }
+ return instruction;
+ }
+
+ private CfInstruction rewriteTypeInstruction(CfTypeInstruction instruction) {
+ DexType rewrittenType = typeMap.getOrDefault(instruction.getType(), instruction.getType());
+ return rewrittenType != instruction.getType() ? instruction.withType(rewrittenType) : null;
+ }
+
+ private CfInstruction rewriteFrameInstruction(CfFrame instruction) {
+ return instruction.asFrame().mapReferenceTypes(type -> typeMap.getOrDefault(type, type));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
index 22eb7b5..4afc01b 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/Base64Test.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.desugar.desugaredlibrary;
import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.SPECIFICATIONS_WITH_CF2CF;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -33,7 +34,7 @@
public static List<Object[]> data() {
return buildParameters(
getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
- ImmutableList.of(JDK11_PATH),
+ ImmutableList.of(JDK11, JDK11_PATH),
SPECIFICATIONS_WITH_CF2CF);
}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
index 089553b..f1e10a1 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DateTimeFormatterTest.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -69,11 +70,13 @@
if (libraryDesugaringSpecification.hasTimeDesugaring(parameters)) {
run.assertSuccessWithOutput(
parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V14_0_0)
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
? expectedOutputDesugaredLibNNBSP
: expectedOutputDesugaredLib);
} else {
run.assertSuccessWithOutput(
parameters.isDexRuntimeVersionNewerThanOrEqual(Version.V14_0_0)
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
? expectedOutputNNBSP
: expectedOutput);
}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
index 8245517..d25f709 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryDeterminismTest.java
@@ -4,7 +4,7 @@
package com.android.tools.r8.desugar.desugaredlibrary;
import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
-import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8AndAll3Jdk11;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -36,7 +36,7 @@
public static List<Object[]> data() {
return buildParameters(
getTestParameters().withDexRuntimes().withAllApiLevels().build(),
- getJdk8Jdk11(),
+ getJdk8AndAll3Jdk11(),
ImmutableList.of(D8_L8DEBUG));
}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java
new file mode 100644
index 0000000..16b1f40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryNavTypeTest.java
@@ -0,0 +1,239 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.D8_L8DEBUG;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DesugaredLibraryNavTypeTest extends DesugaredLibraryTestBase {
+
+ private static final String EXPECTED_RESULT_FORMAT =
+ StringUtils.lines(
+ "java.lang.IllegalArgumentException",
+ "java.lang.RuntimeException java.lang.ClassNotFoundException java.time.XYZ",
+ "Nav class %s.time.LocalDate",
+ "Nav class %s.time.LocalDate[]",
+ "Nav class java.lang.Object",
+ "java.lang.IllegalArgumentException",
+ "java.lang.RuntimeException java.lang.ClassNotFoundException java.time.XYZ",
+ "Nav class %s.time.LocalDate",
+ "Nav class %s.time.LocalDate[]",
+ "Nav class java.lang.Object");
+ private static final String NAV_TYPE = "Landroidx/navigation/NavType;";
+ private static final String NAV_TYPE_COMPANION = "Landroidx/navigation/NavType$Companion;";
+
+ private final TestParameters parameters;
+ private final CompilationSpecification compilationSpecification;
+ private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+ @Parameters(name = "{0}, spec: {1}, {2}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+ ImmutableList.of(JDK11, JDK11_PATH),
+ ImmutableList.of(D8_L8DEBUG));
+ }
+
+ public DesugaredLibraryNavTypeTest(
+ TestParameters parameters,
+ LibraryDesugaringSpecification libraryDesugaringSpecification,
+ CompilationSpecification compilationSpecification) {
+ this.parameters = parameters;
+ this.compilationSpecification = compilationSpecification;
+ this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+ }
+
+ @Test
+ public void testNavType() throws Throwable {
+ testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+ .addProgramClassFileData(getMain(), getNavType(), getNavTypeCompanion())
+ .addKeepMainRule(Main.class)
+ .addKeepRules(
+ "-keep class"
+ + " com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryNavTypeTest.Main"
+ + " { public static LocalDate MIN; }")
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutput(
+ String.format(
+ EXPECTED_RESULT_FORMAT, getPrefix(), getPrefix(), getPrefix(), getPrefix()));
+ }
+
+ private String getPrefix() {
+ return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O) ? "java" : "j$";
+ }
+
+ public static byte[] getNavTypeCompanion() throws Exception {
+ return transformer(NavType.Companion.class)
+ .setClassDescriptor(NAV_TYPE_COMPANION)
+ .replaceClassDescriptorInMembers(descriptor(NavType.class), NAV_TYPE)
+ .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+ .replaceClassDescriptorInMethodInstructions(
+ descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+ .transform();
+ }
+
+ public static byte[] getNavType() throws Exception {
+ return transformer(NavType.class)
+ .setClassDescriptor(NAV_TYPE)
+ .replaceClassDescriptorInMembers(descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+ .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+ .replaceClassDescriptorInMethodInstructions(
+ descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+ .transform();
+ }
+
+ public static byte[] getMain() throws Exception {
+ return transformer(Main.class)
+ .replaceClassDescriptorInMethodInstructions(descriptor(NavType.class), NAV_TYPE)
+ .replaceClassDescriptorInMethodInstructions(
+ descriptor(NavType.Companion.class), NAV_TYPE_COMPANION)
+ .transform();
+ }
+
+ public static class Main {
+
+ // The field should be kept to force keep it on desugared library.
+ public static LocalDate MIN = LocalDate.MIN;
+
+ public static void main(String[] args) {
+ testCallCompanion();
+ testCallStatic();
+ }
+
+ private static void testCallStatic() {
+ // Test IAE are correctly rethrown.
+ try {
+ System.out.println(NavType.fromArgType("IAETest", null));
+ } catch (Throwable t) {
+ System.out.println(t.getClass().getName());
+ }
+ // Test missing class is still missing with the correct error.
+ try {
+ System.out.println(
+ NavType.fromArgType("java.time.XYZ", "com.android.tools.r8.desugar.desugaredlibrary"));
+ } catch (Throwable t) {
+ System.out.println(
+ t.getClass().getName()
+ + " "
+ + t.getCause().getClass().getName()
+ + " "
+ + t.getCause().getMessage());
+ }
+ // Test class is present with the retargeting and desugared library.
+ System.out.println(
+ NavType.fromArgType(
+ "java.time.LocalDate", "com.android.tools.r8.desugar.desugaredlibrary"));
+ // Test array class is present with the retargeting and desugared library.
+ System.out.println(
+ NavType.fromArgType(
+ "java.time.LocalDate[]", "com.android.tools.r8.desugar.desugaredlibrary"));
+ // Test always present class.
+ System.out.println(
+ NavType.fromArgType("java.lang.Object", "com.android.tools.r8.desugar.desugaredlibrary"));
+ }
+
+ private static void testCallCompanion() {
+ // Test IAE are correctly rethrown.
+ try {
+ System.out.println(NavType.Companion.fromArgType("IAETest", null));
+ } catch (Throwable t) {
+ System.out.println(t.getClass().getName());
+ }
+ // Test missing class is still missing with the correct error.
+ try {
+ System.out.println(
+ NavType.Companion.fromArgType(
+ "java.time.XYZ", "com.android.tools.r8.desugar.desugaredlibrary"));
+ } catch (Throwable t) {
+ System.out.println(
+ t.getClass().getName()
+ + " "
+ + t.getCause().getClass().getName()
+ + " "
+ + t.getCause().getMessage());
+ }
+ // Test class is present with the retargeting and desugared library.
+ System.out.println(
+ NavType.Companion.fromArgType(
+ "java.time.LocalDate", "com.android.tools.r8.desugar.desugaredlibrary"));
+ // Test array class is present with the retargeting and desugared library.
+ System.out.println(
+ NavType.Companion.fromArgType(
+ "java.time.LocalDate[]", "com.android.tools.r8.desugar.desugaredlibrary"));
+ // Test always present class.
+ System.out.println(
+ NavType.Companion.fromArgType(
+ "java.lang.Object", "com.android.tools.r8.desugar.desugaredlibrary"));
+ }
+
+ public String run(Supplier<String> supplier) {
+ try {
+ return supplier.get();
+ } catch (Throwable t) {
+ return t.getClass().toString();
+ }
+ }
+ }
+
+ // Will be rewritten to androidx/navigation/NavType.
+ public static class NavType {
+
+ public static final Companion Companion = new Companion();
+
+ private final Class<?> clazz;
+ private final boolean isArray;
+
+ public NavType(Class<?> clazz, boolean isArray) {
+ this.clazz = clazz;
+ this.isArray = isArray;
+ }
+
+ @Override
+ public String toString() {
+ return "Nav " + clazz.toString() + (isArray ? "[]" : "");
+ }
+
+ public static NavType fromArgType(String type, String pkgName) {
+ return Companion.fromArgType(type, pkgName);
+ }
+
+ public static class Companion {
+
+ public NavType fromArgType(String type, String pkgName) {
+ try {
+ String className = (type.startsWith(".") && pkgName != null) ? pkgName + type : type;
+ if (type.equals("IAETest")) {
+ throw new IllegalArgumentException(className + " is not Serializable or Parcelable.");
+ } else {
+ if (type.endsWith("[]")) {
+ className = className.substring(0, className.length() - 2);
+ return new NavType(Class.forName(className), true);
+ } else {
+ return new NavType(Class.forName(className), false);
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java
new file mode 100644
index 0000000..2d7ff31
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/DesugaredLibraryBridge.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.generation;
+
+public class DesugaredLibraryBridge {
+
+ public static NavType<?> fromArgType(
+ NavType.Companion companion, String type, String packageName) {
+ if (type == null || !type.startsWith("java")) {
+ return companion.fromArgType(type, packageName);
+ }
+ // With desugared library j$ types take precedence over java types.
+ try {
+ return companion.fromArgType("j$" + type.substring("java".length()), packageName);
+ } catch (RuntimeException e) {
+ if (e.getCause() instanceof ClassNotFoundException) {
+ return companion.fromArgType(type, packageName);
+ }
+ throw e;
+ }
+ }
+
+ public static class NavType<T> {
+
+ public static final Companion Companion = new Companion();
+
+ public static class Companion {
+ public NavType<?> fromArgType(String type, String packageName) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java
new file mode 100644
index 0000000..bf45240
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/generation/GenerateDesugaredLibraryBridge.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.generation;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cfmethodgeneration.InstructionTypeMapper;
+import com.android.tools.r8.cfmethodgeneration.MethodGenerationBase;
+import com.android.tools.r8.desugar.desugaredlibrary.generation.DesugaredLibraryBridge.NavType;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class GenerateDesugaredLibraryBridge extends MethodGenerationBase {
+
+ private final DexType GENERATED_TYPE =
+ factory.createType(
+ "Lcom/android/tools/r8/ir/desugar/desugaredlibrary/retargeter/DesugaredLibraryCfMethods;");
+ private final List<Class<?>> METHOD_TEMPLATE_CLASSES =
+ ImmutableList.of(DesugaredLibraryBridge.class);
+
+ protected final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withCfRuntime(CfVm.JDK9).build();
+ }
+
+ public GenerateDesugaredLibraryBridge(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Override
+ protected DexType getGeneratedType() {
+ return GENERATED_TYPE;
+ }
+
+ @Override
+ protected List<Class<?>> getMethodTemplateClasses() {
+ return METHOD_TEMPLATE_CLASSES;
+ }
+
+ @Override
+ protected int getYear() {
+ return 2023;
+ }
+
+ @Test
+ public void testDesugaredLibraryBridge() throws Exception {
+ ArrayList<Class<?>> sorted = new ArrayList<>(getMethodTemplateClasses());
+ sorted.sort(Comparator.comparing(Class::getTypeName));
+ assertEquals("Classes should be listed in sorted order", sorted, getMethodTemplateClasses());
+ assertEquals(
+ FileUtils.readTextFile(getGeneratedFile(), StandardCharsets.UTF_8), generateMethods());
+ }
+
+ @Override
+ protected CfCode getCode(String holderName, String methodName, CfCode code) {
+ InstructionTypeMapper instructionTypeMapper =
+ new InstructionTypeMapper(
+ factory,
+ ImmutableMap.of(
+ factory.createType(DescriptorUtils.javaClassToDescriptor(NavType.class)),
+ factory.createType("Landroidx/navigation/NavType;"),
+ factory.createType(DescriptorUtils.javaClassToDescriptor(NavType.Companion.class)),
+ factory.createType("Landroidx/navigation/NavType$Companion;")),
+ Function.identity());
+ code.setInstructions(
+ code.getInstructions().stream()
+ .map(instructionTypeMapper::rewriteInstruction)
+ .collect(Collectors.toList()));
+ return code;
+ }
+
+ public static void main(String[] args) throws Exception {
+ new GenerateDesugaredLibraryBridge(null).generateMethodsAndWriteThemToFile();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
index b82ba3e..de43daf 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/test/LibraryDesugaringSpecification.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
+import java.util.function.Supplier;
import org.junit.rules.TemporaryFolder;
public class LibraryDesugaringSpecification {
@@ -91,22 +92,27 @@
LATEST
}
- private static final Path tempLibraryJDK11Undesugar = createUndesugaredJdk11LibJarForTesting();
+ private static Path tempLibraryJdk11UndesugarCache;
- private static Path createUndesugaredJdk11LibJarForTesting() {
+ private static synchronized Path ensureUndesugaredJdk11LibJarForTesting() {
+ if (tempLibraryJdk11UndesugarCache != null) {
+ return tempLibraryJdk11UndesugarCache;
+ }
try {
TemporaryFolder staticTemp = ToolHelper.getTemporaryFolderForTest();
staticTemp.create();
Path jdklib_desugaring = staticTemp.newFolder("jdklib_desugaring").toPath();
- return DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
- jdklib_desugaring, DESUGARED_JDK_11_LIB_JAR);
+ tempLibraryJdk11UndesugarCache =
+ DesugaredLibraryJDK11Undesugarer.undesugaredJarJDK11(
+ jdklib_desugaring, DESUGARED_JDK_11_LIB_JAR);
+ return tempLibraryJdk11UndesugarCache;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Path getTempLibraryJDK11Undesugar() {
- return tempLibraryJDK11Undesugar;
+ return ensureUndesugaredJdk11LibJarForTesting();
}
// Main head specifications.
@@ -121,7 +127,7 @@
public static LibraryDesugaringSpecification JDK11 =
new LibraryDesugaringSpecification(
"JDK11",
- tempLibraryJDK11Undesugar,
+ LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
"jdk11/desugar_jdk_libs.json",
AndroidApiLevel.R,
JDK11_DESCRIPTOR,
@@ -129,7 +135,7 @@
public static LibraryDesugaringSpecification JDK11_MINIMAL =
new LibraryDesugaringSpecification(
"JDK11_MINIMAL",
- tempLibraryJDK11Undesugar,
+ LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
"jdk11/desugar_jdk_libs_minimal.json",
AndroidApiLevel.R,
EMPTY_DESCRIPTOR_24,
@@ -137,7 +143,7 @@
public static LibraryDesugaringSpecification JDK11_PATH =
new LibraryDesugaringSpecification(
"JDK11_PATH",
- tempLibraryJDK11Undesugar,
+ LibraryDesugaringSpecification::ensureUndesugaredJdk11LibJarForTesting,
"jdk11/desugar_jdk_libs_nio.json",
AndroidApiLevel.R,
JDK11_PATH_DESCRIPTOR,
@@ -165,7 +171,8 @@
new LibraryDesugaringSpecification("1.1.5", AndroidApiLevel.P);
private final String name;
- private final Set<Path> desugarJdkLibs;
+ private final Set<Supplier<Path>> desugarJdkLibsProvider;
+ private Set<Path> desugarJdkLibs;
private final Path specification;
private final Set<Path> libraryFiles;
private final Descriptor descriptor;
@@ -195,7 +202,36 @@
Set<Path> libraryFiles,
Descriptor descriptor,
String extraKeepRules) {
+ this(name, null, desugarJdkLibs, specification, libraryFiles, descriptor, extraKeepRules);
+ }
+
+ private LibraryDesugaringSpecification(
+ String name,
+ Supplier<Path> desugarJdkLibsSupplier,
+ String specificationPath,
+ AndroidApiLevel androidJarLevel,
+ Descriptor descriptor,
+ CustomConversionVersion legacy) {
+ this(
+ name,
+ ImmutableSet.of(desugarJdkLibsSupplier, () -> ToolHelper.getDesugarLibConversions(legacy)),
+ null,
+ Paths.get(ToolHelper.LIBRARY_DESUGAR_SOURCE_DIR + specificationPath),
+ ImmutableSet.of(ToolHelper.getAndroidJar(androidJarLevel)),
+ descriptor,
+ "");
+ }
+
+ private LibraryDesugaringSpecification(
+ String name,
+ Set<Supplier<Path>> desugarJdkLibsSuppliers,
+ Set<Path> desugarJdkLibs,
+ Path specification,
+ Set<Path> libraryFiles,
+ Descriptor descriptor,
+ String extraKeepRules) {
this.name = name;
+ this.desugarJdkLibsProvider = desugarJdkLibsSuppliers;
this.desugarJdkLibs = desugarJdkLibs;
this.specification = specification;
this.libraryFiles = libraryFiles;
@@ -220,7 +256,14 @@
return name;
}
- public Set<Path> getDesugarJdkLibs() {
+ public synchronized Set<Path> getDesugarJdkLibs() {
+ if (desugarJdkLibs == null) {
+ ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
+ for (Supplier<Path> pathSupplier : desugarJdkLibsProvider) {
+ builder.add(pathSupplier.get());
+ }
+ desugarJdkLibs = builder.build();
+ }
return desugarJdkLibs;
}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
index dc3ef4e..14485c9 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentAnnotationsTest.java
@@ -16,6 +16,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.Pair;
@@ -35,7 +36,7 @@
private static final String RECORD_NAME = "RecordWithAnnotations";
private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
- private static final String EXPECTED_RESULT =
+ private static final String JVM_EXPECTED_RESULT =
StringUtils.lines(
"Jane Doe",
"42",
@@ -57,12 +58,39 @@
"@records.RecordWithAnnotations$AnnotationRecordComponentOnly(\"z\")",
"2",
"2",
- "@records.RecordWithAnnotations$Annotation(\"a\")",
- "@records.RecordWithAnnotations$AnnotationFieldOnly(\"b\")",
- "2",
"@records.RecordWithAnnotations$Annotation(\"x\")",
- "@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")");
- private static final String EXPECTED_RESULT_R8 =
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(\"a\")",
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(\"b\")");
+ private static final String ART_EXPECTED_RESULT =
+ StringUtils.lines(
+ "Jane Doe",
+ "42",
+ "Jane Doe",
+ "42",
+ "true",
+ "2",
+ "name",
+ "java.lang.String",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=a)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+ "age",
+ "int",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=x)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+ "2",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=x)",
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(value=y)",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=a)",
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(value=b)");
+ private static final String JVM_EXPECTED_RESULT_R8 =
StringUtils.lines(
"Jane Doe",
"42",
@@ -89,7 +117,34 @@
"2",
"@records.RecordWithAnnotations$Annotation(\"x\")",
"@records.RecordWithAnnotations$AnnotationFieldOnly(\"y\")");
- private static final String EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
+ private static final String ART_EXPECTED_RESULT_R8 =
+ StringUtils.lines(
+ "Jane Doe",
+ "42",
+ "Jane Doe",
+ "42",
+ "true",
+ "2",
+ "a",
+ "java.lang.String",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=a)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+ "b",
+ "int",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=x)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+ "2",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=a)",
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(value=b)",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=x)",
+ "@records.RecordWithAnnotations$AnnotationFieldOnly(value=y)");
+ private static final String JVM_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
StringUtils.lines(
"Jane Doe",
"42",
@@ -112,10 +167,33 @@
"2",
"0",
"0");
- private static final String EXPECTED_RESULT_DESUGARED =
- StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
- private static final String EXPECTED_RESULT_DESUGARED_JVM17 =
+ private static final String ART_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS =
+ StringUtils.lines(
+ "Jane Doe",
+ "42",
+ "Jane Doe",
+ "42",
+ "true",
+ "2",
+ "a",
+ "java.lang.String",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=a)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=c)",
+ "b",
+ "int",
+ "true",
+ "2",
+ "@records.RecordWithAnnotations$Annotation(value=x)",
+ "@records.RecordWithAnnotations$AnnotationRecordComponentOnly(value=z)",
+ "2",
+ "0",
+ "0");
+ private static final String EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT =
StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "false");
+ private static final String EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT =
+ StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
@Parameter(0)
public TestParameters parameters;
@@ -141,7 +219,7 @@
testForJvm(parameters)
.addProgramClassFileData(PROGRAM_DATA)
.run(parameters.getRuntime(), MAIN_TYPE)
- .assertSuccessWithOutput(EXPECTED_RESULT);
+ .assertSuccessWithOutput(JVM_EXPECTED_RESULT);
}
@Test
@@ -151,6 +229,8 @@
// Android U will support records.
boolean compilingForNativeRecordSupport =
parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+ boolean runtimeWithNativeRecordSupport =
+ parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
testForDesugaring(
parameters,
options -> {
@@ -164,16 +244,13 @@
.addProgramClassFileData(PROGRAM_DATA)
.run(parameters.getRuntime(), MAIN_TYPE)
.applyIf(
- parameters.isDexRuntime() && compilingForNativeRecordSupport,
- // Current Art 14 build does not support the java.lang.Record class.
- r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
compilingForNativeRecordSupport,
- r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
+ r -> r.assertSuccessWithOutput(ART_EXPECTED_RESULT),
r ->
r.assertSuccessWithOutput(
- !parameters.isCfRuntime()
- ? EXPECTED_RESULT_DESUGARED
- : EXPECTED_RESULT_DESUGARED_JVM17)
+ !runtimeWithNativeRecordSupport
+ ? EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT
+ : EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT)
.inspect(
inspector -> {
ClassSubject person =
@@ -247,6 +324,9 @@
boolean compilingForNativeRecordSupport =
parameters.isCfRuntime()
|| parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+ boolean runtimeWithNativeRecordSupport =
+ parameters.isCfRuntime()
+ || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V14_0_0);
testForR8(parameters.getBackend())
.addProgramClassFileData(PROGRAM_DATA)
// TODO(b/231930852): Change to android.jar for Androud U when that contains
@@ -317,16 +397,24 @@
})
.run(parameters.getRuntime(), MAIN_TYPE)
.applyIf(
- parameters.isCfRuntime(),
// TODO(b/274888318): EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS still has component
// annotations.
+ parameters.isCfRuntime(),
r ->
r.assertSuccessWithOutput(
- keepAnnotations ? EXPECTED_RESULT_R8 : EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
- // r -> r.assertSuccessWithOutput(EXPECTED_RESULT_R8),
+ keepAnnotations
+ ? JVM_EXPECTED_RESULT_R8
+ : JVM_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
compilingForNativeRecordSupport,
- // Current Art 14 build does not support the java.lang.Record class.
- r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
- r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED));
+ r ->
+ r.assertSuccessWithOutput(
+ keepAnnotations
+ ? ART_EXPECTED_RESULT_R8
+ : ART_EXPECTED_RESULT_R8_NO_KEEP_ANNOTATIONS),
+ r ->
+ r.assertSuccessWithOutput(
+ runtimeWithNativeRecordSupport
+ ? EXPECTED_RESULT_DESUGARED_RECORD_SUPPORT
+ : EXPECTED_RESULT_DESUGARED_NO_RECORD_SUPPORT));
}
}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
index 5ce845f..db46699 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordComponentSignatureTest.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.desugar.records;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@@ -14,6 +15,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.StringUtils;
@@ -56,6 +58,8 @@
"Jane Doe", "42", "Jane Doe", "42", "true", "1", "a", "java.lang.Object", "null", "0");
private static final String EXPECTED_RESULT_DESUGARED =
StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "Class.isRecord not present");
+ private static final String EXPECTED_RESULT_DESUGARED_ART_14 =
+ StringUtils.lines("Jane Doe", "42", "Jane Doe", "42", "false");
@Parameter(0)
public TestParameters parameters;
@@ -91,6 +95,9 @@
// Android U will support records.
boolean compilingForNativeRecordSupport =
parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+ boolean runningWithNativeRecordSupport =
+ parameters.getRuntime().isDex()
+ && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
testForDesugaring(
parameters,
options -> {
@@ -106,9 +113,12 @@
.applyIf(
compilingForNativeRecordSupport,
// Current Art 14 build does not support the java.lang.Record class.
- r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
+ r -> r.assertSuccessWithOutput(EXPECTED_RESULT),
r ->
- r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED)
+ r.assertSuccessWithOutput(
+ runningWithNativeRecordSupport
+ ? EXPECTED_RESULT_DESUGARED_ART_14
+ : EXPECTED_RESULT_DESUGARED)
.inspect(
inspector -> {
ClassSubject person =
@@ -154,6 +164,9 @@
boolean compilingForNativeRecordSupport =
parameters.isCfRuntime()
|| parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U);
+ boolean runningWithNativeRecordSupport =
+ parameters.getRuntime().isDex()
+ && parameters.getRuntime().asDex().getVersion().isNewerThanOrEqual(Version.V14_0_0);
testForR8(parameters.getBackend())
.addProgramClassFileData(PROGRAM_DATA)
// TODO(b/231930852): Change to android.jar for Androud U when that contains
@@ -173,8 +186,8 @@
inspector -> {
ClassSubject person = inspector.clazz("records.RecordWithSignature$Person");
FieldSubject age = person.uniqueFieldWithOriginalName("age");
- assertThat(age, isPresentAndRenamed());
if (compilingForNativeRecordSupport) {
+ assertThat(age, isPresentAndRenamed());
assertEquals(1, person.getFinalRecordComponents().size());
assertEquals(
age.getFinalName(), person.getFinalRecordComponents().get(0).getName());
@@ -187,19 +200,21 @@
}
assertEquals(0, person.getFinalRecordComponents().get(0).getAnnotations().size());
} else {
+ assertThat(age, isAbsent());
assertEquals(0, person.getFinalRecordComponents().size());
}
})
.run(parameters.getRuntime(), MAIN_TYPE)
// No Art VM actually supports the java.lang.Record class.
.applyIf(
- parameters.isCfRuntime(),
+ compilingForNativeRecordSupport,
r ->
r.assertSuccessWithOutput(
keepSignatures ? EXPECTED_RESULT_R8 : EXPECTED_RESULT_R8_NO_KEEP_SIGNATURE),
- compilingForNativeRecordSupport,
- // Current Art 14 build does not support the java.lang.Record class.
- r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class),
- r -> r.assertSuccessWithOutput(EXPECTED_RESULT_DESUGARED));
+ r ->
+ r.assertSuccessWithOutput(
+ runningWithNativeRecordSupport
+ ? EXPECTED_RESULT_DESUGARED_ART_14
+ : EXPECTED_RESULT_DESUGARED));
}
}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
index 3e1c8ce..00a590e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomSplitDesugaringTest.java
@@ -4,9 +4,13 @@
package com.android.tools.r8.desugar.records;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import java.nio.file.Path;
import org.junit.Test;
@@ -77,7 +81,20 @@
.addProgramFiles(desugared)
.setMinApi(parameters)
.addKeepMainRule(MAIN_TYPE)
- .compile()
+ .applyIf(
+ parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U),
+ b -> b.allowDiagnosticWarningMessages())
+ .compileWithExpectedDiagnostics(
+ // Type com.android.tools.r8.RecordTag from desugared code will be converted to
+ // java.lang.Record during reading causing duplicate java.lang.Record class.
+ parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.U)
+ ? diagnostics ->
+ diagnostics.assertWarningsMatch(
+ diagnosticMessage(
+ containsString(
+ "The following library types, prefixed by java., are present both"
+ + " as library and non library classes: java.lang.Record.")))
+ : diagnostics -> diagnostics.assertNoMessages())
.run(parameters.getRuntime(), MAIN_TYPE)
.assertSuccessWithOutput(EXPECTED_RESULT_R8);
}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
index fcbbf70..d97a313 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
@@ -72,9 +72,7 @@
.addKeepRules(enumKeepRules.getKeepRules())
.addKeepRuntimeVisibleAnnotations()
.addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
- // TODO(b/268005228): We should be able to unbox.
- .addEnumUnboxingInspector(
- inspector -> inspector.assertUnboxedIf(!kotlinParameters.isKotlinDev(), PKG + ".Color"))
+ .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(PKG + ".Color"))
.allowDiagnosticMessages()
.setMinApi(parameters)
.compile()
diff --git a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
index bd06608..bda012e 100644
--- a/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
+++ b/src/test/java/com/android/tools/r8/globalsynthetics/GlobalSyntheticsEnsureClassesOutputTest.java
@@ -50,7 +50,7 @@
.setProgramConsumer(new DexIndexedConsumer.ArchiveConsumer(output))
.build());
CodeInspector inspector = new CodeInspector(output);
- assertEquals(1026, inspector.allClasses().size());
+ assertEquals(1044, inspector.allClasses().size());
}
@Test
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
index 1823867..e5434a0 100644
--- a/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
+++ b/src/test/java/com/android/tools/r8/ir/desugar/varhandle/GenerateVarHandleMethods.java
@@ -9,16 +9,11 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.cf.code.CfFieldInstruction;
-import com.android.tools.r8.cf.code.CfFrame;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfTypeInstruction;
+import com.android.tools.r8.cfmethodgeneration.InstructionTypeMapper;
import com.android.tools.r8.cfmethodgeneration.MethodGenerationBase;
import com.android.tools.r8.graph.CfCode;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.utils.DescriptorUtils;
@@ -30,9 +25,7 @@
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -95,74 +88,6 @@
return field;
}
- // TODO(b/261024278): Share this code.
- private class InstructionTypeMapper {
- private final Map<DexType, DexType> typeMap;
- private final Function<String, String> methodNameMap;
-
- InstructionTypeMapper(Map<DexType, DexType> typeMap, Function<String, String> methodNameMap) {
- this.typeMap = typeMap;
- this.methodNameMap = methodNameMap;
- }
-
- private CfInstruction rewriteInstruction(CfInstruction instruction) {
- if (instruction.isTypeInstruction()) {
- CfInstruction rewritten = rewriteTypeInstruction(instruction.asTypeInstruction());
- return rewritten == null ? instruction : rewritten;
- }
- if (instruction.isFieldInstruction()) {
- return rewriteFieldInstruction(instruction.asFieldInstruction());
- }
- if (instruction.isInvoke()) {
- return rewriteInvokeInstruction(instruction.asInvoke());
- }
- if (instruction.isFrame()) {
- return rewriteFrameInstruction(instruction.asFrame());
- }
- return instruction;
- }
-
- private CfInstruction rewriteInvokeInstruction(CfInvoke instruction) {
- CfInvoke invoke = instruction.asInvoke();
- DexMethod method = invoke.getMethod();
- String name = method.getName().toString();
- DexType holderType = invoke.getMethod().getHolderType();
- DexType rewrittenType = typeMap.getOrDefault(holderType, holderType);
- String rewrittenName =
- rewrittenType == factory.varHandleType ? methodNameMap.apply(name) : name;
- if (rewrittenType != holderType) {
- // TODO(b/261024278): If sharing this code also rewrite signature.
- return new CfInvoke(
- invoke.getOpcode(),
- factory.createMethod(
- rewrittenType, invoke.getMethod().getProto(), factory.createString(rewrittenName)),
- invoke.isInterface());
- }
- return instruction;
- }
-
- private CfFieldInstruction rewriteFieldInstruction(CfFieldInstruction instruction) {
- DexType holderType = instruction.getField().getHolderType();
- DexType rewrittenHolderType = typeMap.getOrDefault(holderType, holderType);
- DexType fieldType = instruction.getField().getType();
- DexType rewrittenType = typeMap.getOrDefault(fieldType, fieldType);
- if (rewrittenHolderType != holderType || rewrittenType != fieldType) {
- return instruction.createWithField(
- factory.createField(rewrittenHolderType, rewrittenType, instruction.getField().name));
- }
- return instruction;
- }
-
- private CfInstruction rewriteTypeInstruction(CfTypeInstruction instruction) {
- DexType rewrittenType = typeMap.getOrDefault(instruction.getType(), instruction.getType());
- return rewrittenType != instruction.getType() ? instruction.withType(rewrittenType) : null;
- }
-
- private CfInstruction rewriteFrameInstruction(CfFrame instruction) {
- return instruction.asFrame().mapReferenceTypes(type -> typeMap.getOrDefault(type, type));
- }
- }
-
@Override
protected CfCode getCode(String holderName, String methodName, CfCode code) {
if (methodName.endsWith("Stub")) {
@@ -178,6 +103,7 @@
// sun.misc.Unsafe.
InstructionTypeMapper instructionTypeMapper =
new InstructionTypeMapper(
+ factory,
ImmutableMap.of(
factory.createType(
DescriptorUtils.javaClassToDescriptor(DesugarMethodHandlesLookup.class)),
diff --git a/third_party/android_jar/lib-v34.tar.gz.sha1 b/third_party/android_jar/lib-v34.tar.gz.sha1
index 3b7b739..0c3c782 100644
--- a/third_party/android_jar/lib-v34.tar.gz.sha1
+++ b/third_party/android_jar/lib-v34.tar.gz.sha1
@@ -1 +1 @@
-ac28074fa7b977e03eb0692248e1edcee36a2c33
\ No newline at end of file
+0b366542a5c6536e93da29e5f13bd320df6e2e34
\ No newline at end of file
diff --git a/third_party/api_database/api_database.tar.gz.sha1 b/third_party/api_database/api_database.tar.gz.sha1
index 9883fe5..5749b41 100644
--- a/third_party/api_database/api_database.tar.gz.sha1
+++ b/third_party/api_database/api_database.tar.gz.sha1
@@ -1 +1 @@
-29b5c8dfdccf33e7a540d5de29476805a7d5c2f0
\ No newline at end of file
+9570931632e830d695b18bd83f69456c74196d6e
\ No newline at end of file
diff --git a/third_party/dependencies_new.tar.gz.sha1 b/third_party/dependencies_new.tar.gz.sha1
index 5d89edc..e7d9678 100644
--- a/third_party/dependencies_new.tar.gz.sha1
+++ b/third_party/dependencies_new.tar.gz.sha1
@@ -1 +1 @@
-37f0c05fb0b91effe962a2dde30818c6e5e3cb23
\ No newline at end of file
+197067233a81d8fa441a320c4cad287fa5a3eeb3
\ No newline at end of file
diff --git a/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1 b/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1
new file mode 100644
index 0000000..017f318
--- /dev/null
+++ b/third_party/openjdk/desugar_jdk_libs_releases/2.0.3.tar.gz.sha1
@@ -0,0 +1 @@
+82006808a828dcb64726f119ab93402281fa9f17
\ No newline at end of file
diff --git a/third_party/r8.tar.gz.sha1 b/third_party/r8.tar.gz.sha1
index 4dd64c4..919e57c 100644
--- a/third_party/r8.tar.gz.sha1
+++ b/third_party/r8.tar.gz.sha1
@@ -1 +1 @@
-1ca30cd6edf6ea17a666947b22a60d2fa5cc8d5f
\ No newline at end of file
+1834c0701c4d967fde7e120aee4b2083a39e535e
\ No newline at end of file
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 00f10d8..d6c0dd7 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -32,7 +32,10 @@
ERROR_PRONE_VERSION = '2.18.0'
TESTNG_VERSION = '6.10'
-
+# Resource shrinker dependency versions
+AAPT2_PROTO_VERSION = '8.2.0-alpha10-10154469'
+PROTOBUF_VERSION = '3.19.3'
+STUDIO_SDK_VERSION = '31.2.0-alpha10'
BUILD_DEPENDENCIES = [
'com.google.code.gson:gson:{version}'.format(version = GSON_VERSION),
@@ -65,7 +68,14 @@
'org.jetbrains.kotlin:kotlin-reflect:1.8.10',
'org.jetbrains.kotlin:kotlin-script-runtime:1.8.10',
'org.jetbrains.kotlin:kotlin-tooling-core:1.8.10',
- 'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.0.1'
+ 'net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.0.1',
+
+ # Resource shrinker
+ 'com.android.tools.build:aapt2-proto:{version}'.format(version = AAPT2_PROTO_VERSION),
+ 'com.android.tools.layoutlib:layoutlib-api:{version}'.format(version = STUDIO_SDK_VERSION),
+ 'com.android.tools:common:{version}'.format(version = STUDIO_SDK_VERSION),
+ 'com.android.tools:sdk-common:{version}'.format(version = STUDIO_SDK_VERSION),
+ 'com.google.protobuf:protobuf-java:{version}'.format(version = PROTOBUF_VERSION),
]
def dependencies_tar(dependencies_path):
diff --git a/tools/linux/README.art-versions b/tools/linux/README.art-versions
index 03ddaa3..f813085 100644
--- a/tools/linux/README.art-versions
+++ b/tools/linux/README.art-versions
@@ -69,9 +69,9 @@
art-14 (Android U)
------------------
-Build branch udc-preview1-release.
+Build branch udc-beta3-release. Art at 1f3514c28caf4537fe0fdf14559991ca23cf9e30.
-export BRANCH=udc-preview1-release
+export BRANCH=udc-beta3-release
mkdir ${BRANCH}
cd ${BRANCH}
export ANDROID_CHECKOUT=$(pwd)
@@ -90,10 +90,10 @@
cd <r8 checkout>
scripts/update-host-art.sh \
--android-checkout $ANDROID_CHECKOUT \
- --art-dir host/art-14.0.0-dp1 \
+ --art-dir host/art-14.0.0-beta3 \
--android-product redfin
- (cd tools/linux/host; upload_to_google_storage.py -a --bucket r8-deps art-14.0.0-dp1)
+ (cd tools/linux/host; upload_to_google_storage.py -a --bucket r8-deps art-14.0.0-beta3)
art-13 (Android T)
diff --git a/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1 b/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1
new file mode 100644
index 0000000..42f9f43
--- /dev/null
+++ b/tools/linux/host/art-14.0.0-beta3.tar.gz.sha1
@@ -0,0 +1 @@
+c879ddf6410159a4ac7e673fd66a64abe2140ffa
\ No newline at end of file
diff --git a/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1 b/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1
deleted file mode 100644
index c88b923..0000000
--- a/tools/linux/host/art-14.0.0-dp1.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-1c41f5d79e335ff132b7a95d995b65581c250df8
\ No newline at end of file