High-level structure for new repackaging support
Bug: 165783399
Change-Id: Ica95f07624b646fa1c84ae7245c9272661e1db11
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 4d1a076..19b353c 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -76,6 +76,7 @@
import com.android.tools.r8.optimize.MemberRebindingAnalysis;
import com.android.tools.r8.optimize.VisibilityBridgeRemover;
import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.repackaging.Repackaging;
import com.android.tools.r8.shaking.AbstractMethodRemover;
import com.android.tools.r8.shaking.AnnotationRemover;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -796,6 +797,12 @@
}
}
+ // Perform repackaging.
+ // TODO(b/165783399): Consider making repacking available without minification.
+ if (appView.options().isMinifying()) {
+ new Repackaging(appView.withLiveness()).run(executorService, timing);
+ }
+
// Perform minification.
NamingLens namingLens;
if (options.getProguardConfiguration().hasApplyMappingFile()) {
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index a0a1b99..8792d0c 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -128,6 +128,10 @@
return !isPublic() && !isPrivate() && !isProtected();
}
+ public boolean isPackagePrivateOrProtected() {
+ return !isPublic() && !isPrivate();
+ }
+
public boolean isPublic() {
return isSet(Constants.ACC_PUBLIC);
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index ae39752..fe2eabd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -108,6 +108,11 @@
}
}
+ @Override
+ public ClassAccessFlags getAccessFlags() {
+ return accessFlags;
+ }
+
public Iterable<DexEncodedField> fields() {
return fields(Predicates.alwaysTrue());
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexDefinition.java b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
index 09daf79..6168bd8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDefinition.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
@@ -25,6 +25,8 @@
return annotations;
}
+ public abstract AccessFlags<?> getAccessFlags();
+
public DexAnnotationSet liveAnnotations(AppView<AppInfoWithLiveness> appView) {
return annotations.keepIf(
annotation -> AnnotationRemover.shouldKeepAnnotation(appView, this, annotation));
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
index e7d5f02..638a9be 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -14,8 +14,6 @@
return toReference().holder;
}
- public abstract AccessFlags<?> getAccessFlags();
-
@Override
public abstract R toReference();
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramPackage.java b/src/main/java/com/android/tools/r8/graph/ProgramPackage.java
new file mode 100644
index 0000000..28fc4e2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ProgramPackage.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2020, 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.graph;
+
+import com.google.common.collect.Sets;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ProgramPackage implements Iterable<DexProgramClass> {
+
+ private final String packageDescriptor;
+ private final Set<DexProgramClass> classes = Sets.newIdentityHashSet();
+
+ public ProgramPackage(String packageDescriptor) {
+ this.packageDescriptor = packageDescriptor;
+ }
+
+ public void add(DexProgramClass clazz) {
+ assert clazz.getType().getPackageDescriptor().equals(packageDescriptor);
+ classes.add(clazz);
+ }
+
+ public void forEachClass(Consumer<DexProgramClass> consumer) {
+ forEach(consumer);
+ }
+
+ public void forEachField(Consumer<ProgramField> consumer) {
+ forEach(clazz -> clazz.forEachProgramField(consumer));
+ }
+
+ public void forEachMethod(Consumer<ProgramMethod> consumer) {
+ forEach(clazz -> clazz.forEachProgramMethod(consumer));
+ }
+
+ @Override
+ public Iterator<DexProgramClass> iterator() {
+ return classes.iterator();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java b/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java
new file mode 100644
index 0000000..57fc75a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2020, 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.graph;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class ProgramPackageCollection implements Iterable<ProgramPackage> {
+
+ private final Map<String, ProgramPackage> packages;
+
+ private ProgramPackageCollection(Map<String, ProgramPackage> packages) {
+ this.packages = packages;
+ }
+
+ public static ProgramPackageCollection create(AppView<?> appView) {
+ Map<String, ProgramPackage> packages = new HashMap<>();
+ assert !appView.appInfo().getSyntheticItems().hasPendingSyntheticClasses();
+ for (DexProgramClass clazz : appView.appInfo().classes()) {
+ packages
+ .computeIfAbsent(clazz.getType().getPackageDescriptor(), ProgramPackage::new)
+ .add(clazz);
+ }
+ return new ProgramPackageCollection(packages);
+ }
+
+ @Override
+ public Iterator<ProgramPackage> iterator() {
+ return packages.values().iterator();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/SyntheticItems.java b/src/main/java/com/android/tools/r8/graph/SyntheticItems.java
index 28bb51a..20c073a 100644
--- a/src/main/java/com/android/tools/r8/graph/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/graph/SyntheticItems.java
@@ -40,6 +40,10 @@
return new SyntheticItems(ImmutableSet.of());
}
+ public boolean hasPendingSyntheticClasses() {
+ return !pendingClasses.isEmpty();
+ }
+
public Collection<DexProgramClass> getPendingSyntheticClasses() {
return Collections.unmodifiableCollection(pendingClasses.values());
}
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
new file mode 100644
index 0000000..8523937
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2020, 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.repackaging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramPackage;
+import com.android.tools.r8.graph.ProgramPackageCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.utils.Timing;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Entry-point for supporting the -repackageclasses and -flattenpackagehierarchy directives.
+ *
+ * <p>This pass moves all classes in the program into a user-specified package. Some classes may not
+ * be allowed to be renamed, and thus must remain in the original package.
+ *
+ * <p>A complication is that there can be (i) references to package-private or protected items that
+ * must remain in the package, and (ii) references from methods that must remain in the package to
+ * package-private or protected items. To ensure that such references remain valid after
+ * repackaging, an analysis is run that finds the minimal set of classes that must remain in the
+ * original package due to accessibility constraints.
+ */
+public class Repackaging {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final ProguardConfiguration proguardConfiguration;
+
+ public Repackaging(AppView<AppInfoWithLiveness> appView) {
+ this.appView = appView;
+ this.proguardConfiguration = appView.options().getProguardConfiguration();
+ }
+
+ public void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+ timing.begin("Repackage classes");
+ run(executorService);
+ timing.end();
+ }
+
+ private void run(ExecutorService executorService) throws ExecutionException {
+ if (proguardConfiguration.getPackageObfuscationMode().isNone()) {
+ return;
+ }
+
+ // For each package, find the set of classes that can be repackaged, and move them to the
+ // desired namespace.
+ ProgramPackageCollection packages = ProgramPackageCollection.create(appView);
+ for (ProgramPackage pkg : packages) {
+ Iterable<DexProgramClass> classesToRepackage =
+ computeClassesToRepackage(pkg, executorService);
+ // TODO(b/165783399): Move each class in `classesToRepackage`.
+ // TODO(b/165783399): Investigate if repackaging can lead to different dynamic dispatch. See,
+ // for example, CrossPackageInvokeSuperToPackagePrivateMethodTest.
+ }
+ }
+
+ private Iterable<DexProgramClass> computeClassesToRepackage(
+ ProgramPackage pkg, ExecutorService executorService) throws ExecutionException {
+ // If all classes can be minified, then we are free to move all the classes to a new package.
+ if (isMinificationAllowedForAllClasses(pkg)) {
+ return pkg;
+ }
+
+ // Otherwise, only a subset of classes in the package can be repackaged, and we need to ensure
+ // that package-private accesses continue to work after the split.
+ RepackagingConstraintGraph constraintGraph = new RepackagingConstraintGraph(appView, pkg);
+ boolean hasPackagePrivateOrProtectedItem = constraintGraph.initializeGraph();
+ if (!hasPackagePrivateOrProtectedItem) {
+ return pkg;
+ }
+ constraintGraph.populateConstraints(executorService);
+ return constraintGraph.computeClassesToRepackage();
+ }
+
+ private boolean isMinificationAllowedForAllClasses(ProgramPackage pkg) {
+ AppInfoWithLiveness appInfo = appView.appInfo();
+ for (DexProgramClass clazz : pkg) {
+ if (!appInfo.isMinificationAllowed(clazz.getType())) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java
new file mode 100644
index 0000000..52a9b86
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2020, 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.repackaging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexDefinition;
+import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.ProgramPackage;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * An undirected graph that contains a node for each class, field, and method in a given package.
+ *
+ * <p>An edge X <-> Y is added if X contains a reference to Y and Y is only accessible to X if X and
+ * Y are in the same package.
+ *
+ * <p>Once the graph is populated, we compute the set of reachable nodes from the set of root nodes
+ * that cannot be repackaged due to a -keep rule. The remaining nodes in the graph can all be
+ * repackaged.
+ */
+public class RepackagingConstraintGraph {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final ProgramPackage pkg;
+ private final Map<DexDefinition, Node> nodes = new IdentityHashMap<>();
+
+ public RepackagingConstraintGraph(AppView<AppInfoWithLiveness> appView, ProgramPackage pkg) {
+ this.appView = appView;
+ this.pkg = pkg;
+ }
+
+ public boolean initializeGraph() {
+ // Add all the items in the package into the graph. This way we know which items belong to the
+ // package without having to extract package descriptor strings and comparing them with the
+ // package descriptor.
+ boolean hasPackagePrivateOrProtectedItem = false;
+ for (DexProgramClass clazz : pkg) {
+ nodes.put(clazz, new Node(clazz));
+ hasPackagePrivateOrProtectedItem |= clazz.getAccessFlags().isPackagePrivateOrProtected();
+ for (DexEncodedMember<?, ?> member : clazz.members()) {
+ nodes.put(member, new Node(member));
+ hasPackagePrivateOrProtectedItem |= member.getAccessFlags().isPackagePrivateOrProtected();
+ }
+ }
+ return hasPackagePrivateOrProtectedItem;
+ }
+
+ public void populateConstraints(ExecutorService executorService) throws ExecutionException {
+ // Concurrently add references from methods to the graph.
+ ThreadUtils.processItems(
+ pkg::forEachMethod, this::registerReferencesFromMethod, executorService);
+
+ // TODO(b/165783399): Evaluate if it is worth to parallelize this. The work per field and class
+ // should be little, so it may not be.
+ pkg.forEachClass(this::registerReferencesFromClass);
+ pkg.forEachField(this::registerReferencesFromField);
+ }
+
+ private void registerReferencesFromClass(DexProgramClass clazz) {
+ // TODO(b/165783399): Trace the references to the immediate super types.
+ // TODO(b/165783399): Maybe trace the references in the nest host and/or members.
+ // TODO(b/165783399): Maybe trace the references to the inner classes.
+ // TODO(b/165783399): Maybe trace the references in @kotlin.Metadata.
+ }
+
+ private void registerReferencesFromField(ProgramField field) {
+ // TODO(b/165783399): Trace the type of the field.
+ // TODO(b/165783399): Trace the references in the field annotations.
+ }
+
+ private void registerReferencesFromMethod(ProgramMethod method) {
+ // TODO(b/165783399): Trace the type references in the method signature.
+ // TODO(b/165783399): Trace the references in the method and method parameter annotations.
+ DexEncodedMethod definition = method.getDefinition();
+ if (definition.hasCode()) {
+ RepackagingUseRegistry registry = new RepackagingUseRegistry(appView, this, method);
+ definition.getCode().registerCodeReferences(method, registry);
+ }
+ }
+
+ public Iterable<DexProgramClass> computeClassesToRepackage() {
+ // TODO(b/165783399): From each node in the graph that cannot be moved elsewhere due to a -keep
+ // rule, mark all neighbors as pinned, and repeat.
+ return Collections.emptyList();
+ }
+
+ private static class Node {
+
+ private final DexDefinition definition;
+
+ private final Set<Node> neighbors = Sets.newIdentityHashSet();
+
+ private Node(DexDefinition definition) {
+ this.definition = definition;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
new file mode 100644
index 0000000..3ffd5c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2020, 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.repackaging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class RepackagingUseRegistry extends UseRegistry {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final RepackagingConstraintGraph constraintGraph;
+ private final ProgramMethod method;
+
+ public RepackagingUseRegistry(
+ AppView<AppInfoWithLiveness> appView,
+ RepackagingConstraintGraph constraintGraph,
+ ProgramMethod method) {
+ super(appView.dexItemFactory());
+ this.appView = appView;
+ this.constraintGraph = constraintGraph;
+ this.method = method;
+ }
+
+ @Override
+ public boolean registerInitClass(DexType type) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeVirtual(DexMethod method) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeDirect(DexMethod method) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeStatic(DexMethod method) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeInterface(DexMethod method) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeSuper(DexMethod method) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInstanceFieldRead(DexField field) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInstanceFieldWrite(DexField field) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerNewInstance(DexType type) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerStaticFieldRead(DexField field) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerStaticFieldWrite(DexField field) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerTypeReference(DexType type) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+
+ @Override
+ public boolean registerInstanceOf(DexType type) {
+ // TODO(b/165783399): Add reference-edges to the graph.
+ return false;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 54c1055..ffeddac 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1068,7 +1068,19 @@
// Repackaging all classes into the single user-given (or top-level) package.
REPACKAGE,
// Repackaging all packages into the single user-given (or top-level) package.
- FLATTEN
+ FLATTEN;
+
+ public boolean isNone() {
+ return this == NONE;
+ }
+
+ public boolean isFlattenPackageHierarchy() {
+ return this == FLATTEN;
+ }
+
+ public boolean isRepackageClasses() {
+ return this == REPACKAGE;
+ }
}
public static class OutlineOptions {