blob: e193f4c9981ab5fe2fc2d498fd71553b76c7980c [file] [log] [blame]
// 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 static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.utils.DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR;
import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedMember;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DirectMappedDexApplication;
import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.graph.NestedGraphLens;
import com.android.tools.r8.graph.ProgramPackage;
import com.android.tools.r8.graph.ProgramPackageCollection;
import com.android.tools.r8.graph.SortedProgramPackageCollection;
import com.android.tools.r8.graph.TreeFixerBase;
import com.android.tools.r8.naming.Minifier.MinificationPackageNamingStrategy;
import com.android.tools.r8.repackaging.RepackagingLens.Builder;
import com.android.tools.r8.shaking.AnnotationFixer;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
private final RepackagingConfiguration repackagingConfiguration;
public Repackaging(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
this.proguardConfiguration = appView.options().getProguardConfiguration();
this.repackagingConfiguration =
appView.options().testing.repackagingConfigurationFactory.apply(appView);
}
public RepackagingLens run(
DirectMappedDexApplication.Builder appBuilder, ExecutorService executorService, Timing timing)
throws ExecutionException {
timing.begin("Repackage classes");
RepackagingLens lens = run(appBuilder, executorService);
timing.end();
return lens;
}
public static boolean verifyIdentityRepackaging(AppView<AppInfoWithLiveness> appView) {
// Running the tree fixer with an identity mapping helps ensure that the fixup of of items is
// complete as the rewrite replaces all items regardless of repackaging.
// The identity mapping should result in no move callbacks being called.
Collection<DexProgramClass> newProgramClasses =
new TreeFixerBase(appView) {
@Override
public DexType mapClassType(DexType type) {
return type;
}
@Override
public void recordFieldChange(DexField from, DexField to) {
assert false;
}
@Override
public void recordMethodChange(DexMethod from, DexMethod to) {
assert false;
}
@Override
public void recordClassChange(DexType from, DexType to) {
assert false;
}
}.fixupClasses(appView.appInfo().classesWithDeterministicOrder());
NestedGraphLens emptyRepackagingLens =
new NestedGraphLens(appView) {
@Override
protected boolean isLegitimateToHaveEmptyMappings() {
return true;
}
};
DirectMappedDexApplication newApplication =
appView
.appInfo()
.app()
.asDirect()
.builder()
.replaceProgramClasses(new ArrayList<>(newProgramClasses))
.build();
appView.rewriteWithLensAndApplication(emptyRepackagingLens, newApplication);
return true;
}
private RepackagingLens run(
DirectMappedDexApplication.Builder appBuilder, ExecutorService executorService)
throws ExecutionException {
if (proguardConfiguration.getPackageObfuscationMode().isNone()) {
return null;
}
BiMap<DexType, DexType> mappings = HashBiMap.create();
Map<String, String> packageMappings = new HashMap<>();
Set<String> seenPackageDescriptors = new HashSet<>();
ProgramPackageCollection packages =
SortedProgramPackageCollection.createWithAllProgramClasses(appView);
processPackagesInDesiredLocation(packages, mappings, packageMappings, seenPackageDescriptors);
processRemainingPackages(
packages, mappings, packageMappings, seenPackageDescriptors, executorService);
mappings.entrySet().removeIf(entry -> entry.getKey() == entry.getValue());
if (mappings.isEmpty()) {
return null;
}
RepackagingLens.Builder lensBuilder = new RepackagingLens.Builder();
RepackagingTreeFixer repackagingTreeFixer =
new RepackagingTreeFixer(appView, mappings, lensBuilder);
List<DexProgramClass> newProgramClasses =
new ArrayList<>(
repackagingTreeFixer.fixupClasses(appView.appInfo().classesWithDeterministicOrder()));
appBuilder.replaceProgramClasses(newProgramClasses);
RepackagingLens lens = lensBuilder.build(appView, packageMappings);
new AnnotationFixer(lens).run(appBuilder.getProgramClasses());
return lens;
}
private static class RepackagingTreeFixer extends TreeFixerBase {
private final BiMap<DexType, DexType> mappings;
private final Builder lensBuilder;
public RepackagingTreeFixer(
AppView<AppInfoWithLiveness> appView,
BiMap<DexType, DexType> mappings,
Builder lensBuilder) {
super(appView);
assert mappings != null;
this.mappings = mappings;
this.lensBuilder = lensBuilder;
recordFailedResolutionChanges();
}
@Override
public DexType mapClassType(DexType type) {
return mappings.getOrDefault(type, type);
}
@Override
public void recordFieldChange(DexField from, DexField to) {
lensBuilder.recordMove(from, to);
}
@Override
public void recordMethodChange(DexMethod from, DexMethod to) {
lensBuilder.recordMove(from, to);
}
@Override
public void recordClassChange(DexType from, DexType to) {
lensBuilder.recordMove(from, to);
}
}
private void processPackagesInDesiredLocation(
ProgramPackageCollection packages,
BiMap<DexType, DexType> mappings,
Map<String, String> packageMappings,
Set<String> seenPackageDescriptors) {
// For each package that is already in the desired location, record all the classes from the
// package in the mapping for collision detection.
Iterator<ProgramPackage> iterator = packages.iterator();
while (iterator.hasNext()) {
ProgramPackage pkg = iterator.next();
if (repackagingConfiguration.isPackageInTargetLocation(pkg)) {
for (DexProgramClass alreadyRepackagedClass : pkg) {
if (!appView.appInfo().isRepackagingAllowed(alreadyRepackagedClass)) {
mappings.put(alreadyRepackagedClass.getType(), alreadyRepackagedClass.getType());
}
}
for (DexProgramClass alreadyRepackagedClass : pkg) {
processClass(alreadyRepackagedClass, pkg, pkg.getPackageDescriptor(), mappings);
}
packageMappings.put(pkg.getPackageDescriptor(), pkg.getPackageDescriptor());
seenPackageDescriptors.add(pkg.getPackageDescriptor());
iterator.remove();
}
}
}
private void processRemainingPackages(
ProgramPackageCollection packages,
BiMap<DexType, DexType> mappings,
Map<String, String> packageMappings,
Set<String> seenPackageDescriptors,
ExecutorService executorService)
throws ExecutionException {
// For each package, find the set of classes that can be repackaged, and move them to the
// desired package. We iterate all packages first to see if any classes are pinned and cannot
// be moved, to properly reserve their package.
Map<ProgramPackage, Collection<DexProgramClass>> packagesWithClassesToRepackage =
new IdentityHashMap<>();
for (ProgramPackage pkg : packages) {
Collection<DexProgramClass> classesToRepackage =
computeClassesToRepackage(pkg, executorService);
packagesWithClassesToRepackage.put(pkg, classesToRepackage);
// Reserve the package name to ensure that we are not renaming to a package we cannot move.
if (classesToRepackage.size() != pkg.classesInPackage().size()) {
seenPackageDescriptors.add(pkg.getPackageDescriptor());
}
}
for (ProgramPackage pkg : packages) {
Collection<DexProgramClass> classesToRepackage = packagesWithClassesToRepackage.get(pkg);
if (classesToRepackage.isEmpty()) {
continue;
}
// Already processed packages should have been removed.
String newPackageDescriptor =
repackagingConfiguration.getNewPackageDescriptor(pkg, seenPackageDescriptors);
assert !repackagingConfiguration.isPackageInTargetLocation(pkg);
for (DexProgramClass classToRepackage : classesToRepackage) {
processClass(classToRepackage, pkg, newPackageDescriptor, mappings);
}
seenPackageDescriptors.add(newPackageDescriptor);
// Package remapping is used for adapting resources. If we cannot repackage all classes in
// a package then we put in the original descriptor to ensure that resources are not
// rewritten.
packageMappings.put(
pkg.getPackageDescriptor(),
classesToRepackage.size() == pkg.classesInPackage().size()
? newPackageDescriptor
: pkg.getPackageDescriptor());
}
}
private void processClass(
DexProgramClass classToRepackage,
ProgramPackage pkg,
String newPackageDescriptor,
BiMap<DexType, DexType> mappings) {
// Check if the class has already been processed.
if (mappings.containsKey(classToRepackage.getType())) {
return;
}
// Always repackage outer classes first, if any.
InnerClassAttribute innerClassAttribute = classToRepackage.getInnerClassAttributeForThisClass();
DexProgramClass outerClass = null;
if (innerClassAttribute != null && innerClassAttribute.getOuter() != null) {
outerClass = asProgramClassOrNull(appView.definitionFor(innerClassAttribute.getOuter()));
if (outerClass != null) {
if (pkg.contains(outerClass)) {
processClass(outerClass, pkg, newPackageDescriptor, mappings);
} else {
outerClass = null;
}
}
}
mappings.put(
classToRepackage.getType(),
repackagingConfiguration.getRepackagedType(
classToRepackage, outerClass, newPackageDescriptor, mappings));
}
private Collection<DexProgramClass> computeClassesToRepackage(
ProgramPackage pkg, ExecutorService executorService) throws ExecutionException {
RepackagingConstraintGraph constraintGraph = new RepackagingConstraintGraph(appView, pkg);
boolean canRepackageAllClasses = constraintGraph.initializeGraph();
if (canRepackageAllClasses) {
return pkg.classesInPackage();
}
constraintGraph.populateConstraints(executorService);
return constraintGraph.computeClassesToRepackage();
}
public interface RepackagingConfiguration {
String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors);
boolean isPackageInTargetLocation(ProgramPackage pkg);
DexType getRepackagedType(
DexProgramClass clazz,
DexProgramClass outerClass,
String newPackageDescriptor,
BiMap<DexType, DexType> mappings);
}
public static class DefaultRepackagingConfiguration implements RepackagingConfiguration {
private final AppView<?> appView;
private final DexItemFactory dexItemFactory;
private final ProguardConfiguration proguardConfiguration;
private final MinificationPackageNamingStrategy packageMinificationStrategy;
public DefaultRepackagingConfiguration(AppView<?> appView) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.proguardConfiguration = appView.options().getProguardConfiguration();
this.packageMinificationStrategy = new MinificationPackageNamingStrategy(appView);
}
@Override
public String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
String newPackageDescriptor =
DescriptorUtils.getBinaryNameFromJavaType(proguardConfiguration.getPackagePrefix());
PackageObfuscationMode packageObfuscationMode =
proguardConfiguration.getPackageObfuscationMode();
if (packageObfuscationMode.isRepackageClasses()) {
return newPackageDescriptor;
} else if (packageObfuscationMode.isMinification()) {
assert !proguardConfiguration.hasApplyMappingFile();
// Always keep top-level classes since there packages can never be minified.
if (pkg.getPackageDescriptor().equals("")
|| proguardConfiguration.getKeepPackageNamesPatterns().matches(pkg)
|| mayHavePinnedPackagePrivateOrProtectedItem(pkg)) {
return pkg.getPackageDescriptor();
}
// Plain minification do not support using a specified package prefix.
newPackageDescriptor = "";
} else {
assert packageObfuscationMode.isFlattenPackageHierarchy();
if (!newPackageDescriptor.isEmpty()) {
newPackageDescriptor += DESCRIPTOR_PACKAGE_SEPARATOR;
}
}
return packageMinificationStrategy.next(
newPackageDescriptor, seenPackageDescriptors::contains);
}
@Override
public boolean isPackageInTargetLocation(ProgramPackage pkg) {
String newPackageDescriptor =
DescriptorUtils.getBinaryNameFromJavaType(proguardConfiguration.getPackagePrefix());
PackageObfuscationMode packageObfuscationMode =
proguardConfiguration.getPackageObfuscationMode();
if (packageObfuscationMode.isRepackageClasses()) {
return pkg.getPackageDescriptor().equals(newPackageDescriptor);
} else if (packageObfuscationMode.isMinification()) {
// Always keep top-level classes since there packages can never be minified.
return pkg.getPackageDescriptor().equals("")
|| proguardConfiguration.getKeepPackageNamesPatterns().matches(pkg)
|| mayHavePinnedPackagePrivateOrProtectedItem(pkg);
} else {
assert packageObfuscationMode.isFlattenPackageHierarchy();
// For flatten we will move the package into the package specified by the prefix so we can
// always minify the last part.
return false;
}
}
private boolean mayHavePinnedPackagePrivateOrProtectedItem(ProgramPackage pkg) {
// Go through all package classes and members to see if there is a pinned package-private
// item, in which case we cannot move it because there may be a reflective access to it.
for (DexProgramClass clazz : pkg.classesInPackage()) {
if (clazz.getAccessFlags().isPackagePrivateOrProtected()
&& appView.getKeepInfo().getClassInfo(clazz).isPinned()) {
return true;
}
for (DexEncodedMember<?, ?> member : clazz.members()) {
if (member.getAccessFlags().isPackagePrivateOrProtected()
&& appView.getKeepInfo().getMemberInfo(member, clazz).isPinned()) {
return true;
}
}
}
return false;
}
@Override
public DexType getRepackagedType(
DexProgramClass clazz,
DexProgramClass outerClass,
String newPackageDescriptor,
BiMap<DexType, DexType> mappings) {
DexType repackagedDexType =
clazz.getType().replacePackage(newPackageDescriptor, dexItemFactory);
// Rename the class consistently with its outer class.
if (outerClass != null) {
String simpleName = clazz.getType().getSimpleName();
String outerClassSimpleName = outerClass.getType().getSimpleName();
if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
String newSimpleName =
mappings.get(outerClass.getType()).getSimpleName()
+ simpleName.substring(outerClassSimpleName.length());
repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
} else {
assert false
: "Unexpected name for inner class: "
+ clazz.getType().toSourceString()
+ " (outer class: "
+ outerClass.getType().toSourceString()
+ ")";
}
}
// Ensure that the generated name is unique.
DexType finalRepackagedDexType = repackagedDexType;
for (int i = 1; isRepackageTypeUsed(finalRepackagedDexType, mappings, appView); i++) {
finalRepackagedDexType = repackagedDexType.addSuffix(i + "", dexItemFactory);
}
return finalRepackagedDexType;
}
}
private static boolean isRepackageTypeUsed(
DexType type, BiMap<DexType, DexType> mappings, AppView<?> appView) {
return mappings.inverse().containsKey(type)
|| (appView.hasLiveness() && appView.withLiveness().appInfo().wasPruned(type));
}
/** Testing only. */
public static class SuffixRenamingRepackagingConfiguration implements RepackagingConfiguration {
private final String classNameSuffix;
private final DexItemFactory dexItemFactory;
public SuffixRenamingRepackagingConfiguration(
String classNameSuffix, DexItemFactory dexItemFactory) {
this.classNameSuffix = classNameSuffix;
this.dexItemFactory = dexItemFactory;
}
@Override
public String getNewPackageDescriptor(ProgramPackage pkg, Set<String> seenPackageDescriptors) {
// Don't change the package of classes.
return pkg.getPackageDescriptor();
}
@Override
public boolean isPackageInTargetLocation(ProgramPackage pkg) {
return true;
}
@Override
public DexType getRepackagedType(
DexProgramClass clazz,
DexProgramClass outerClass,
String newPackageDescriptor,
BiMap<DexType, DexType> mappings) {
DexType repackagedDexType = clazz.getType();
// Rename the class consistently with its outer class.
if (outerClass != null) {
String simpleName = clazz.getType().getSimpleName();
String outerClassSimpleName = outerClass.getType().getSimpleName();
if (simpleName.startsWith(outerClassSimpleName + INNER_CLASS_SEPARATOR)) {
String newSimpleName =
mappings.get(outerClass.getType()).getSimpleName()
+ simpleName.substring(outerClassSimpleName.length());
repackagedDexType = repackagedDexType.withSimpleName(newSimpleName, dexItemFactory);
}
}
// Append the class name suffix to all classes.
repackagedDexType = repackagedDexType.addSuffix(classNameSuffix, dexItemFactory);
// Ensure that the generated name is unique.
DexType finalRepackagedDexType = repackagedDexType;
for (int i = 1; mappings.inverse().containsKey(finalRepackagedDexType); i++) {
finalRepackagedDexType =
repackagedDexType.addSuffix(
Character.toString(INNER_CLASS_SEPARATOR) + i, dexItemFactory);
}
return finalRepackagedDexType;
}
}
}