blob: 021fb93aac1dcf3c2017ba5c848ec14cbd2751bb [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.android;
import static com.facebook.buck.android.SmartDexingStep.DexInputHashesProvider;
import com.facebook.buck.dalvik.CanaryFactory;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.java.classes.FileLike;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.ImmutableSha1HashCode;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Responsible for bucketing pre-dexed objects into primary and secondary dex files.
*/
public class PreDexedFilesSorter {
private final Optional<DexWithClasses> rDotJavaDex;
private final List<DexWithClasses> dexFilesToMerge;
private final ClassNameFilter primaryDexFilter;
private final long linearAllocHardLimit;
private final DexStore dexStore;
private final Path secondaryDexJarFilesDir;
/**
* Directory under the project filesystem where this step may write temporary data. This directory
* must exist and be empty before this step writes to it.
*/
private final Path scratchDirectory;
public PreDexedFilesSorter(
Optional<DexWithClasses> rDotJavaDex,
List<DexWithClasses> dexFilesToMerge,
ImmutableSet<String> primaryDexPatterns,
Path scratchDirectory,
long linearAllocHardLimit,
DexStore dexStore,
Path secondaryDexJarFilesDir) {
this.rDotJavaDex = rDotJavaDex;
this.dexFilesToMerge = dexFilesToMerge;
this.primaryDexFilter = ClassNameFilter.fromConfiguration(primaryDexPatterns);
this.scratchDirectory = scratchDirectory;
Preconditions.checkState(linearAllocHardLimit > 0);
this.linearAllocHardLimit = linearAllocHardLimit;
this.dexStore = dexStore;
this.secondaryDexJarFilesDir = secondaryDexJarFilesDir;
}
public Result sortIntoPrimaryAndSecondaryDexes(
BuildContext context,
ImmutableList.Builder<Step> steps) {
List<DexWithClasses> primaryDexContents = Lists.newArrayList();
List<List<DexWithClasses>> secondaryDexesContents = Lists.newArrayList();
int primaryDexSize = 0;
// R.class files should always be in the primary dex.
if (rDotJavaDex.isPresent()) {
primaryDexSize += rDotJavaDex.get().getSizeEstimate();
primaryDexContents.add(rDotJavaDex.get());
}
// Sort dex files so that there's a better chance of the same set of pre-dexed files to end up
// in a given secondary dex file.
ImmutableList<DexWithClasses> sortedDexFilesToMerge = FluentIterable.from(dexFilesToMerge)
.toSortedList(DexWithClasses.DEX_WITH_CLASSES_COMPARATOR);
// Bucket each DexWithClasses into the appropriate dex file.
List<DexWithClasses> currentSecondaryDexContents = null;
int currentSecondaryDexSize = 0;
for (DexWithClasses dexWithClasses : sortedDexFilesToMerge) {
if (mustBeInPrimaryDex(dexWithClasses)) {
// Case 1: Entry must be in the primary dex.
primaryDexSize += dexWithClasses.getSizeEstimate();
if (primaryDexSize > linearAllocHardLimit) {
context.logError(
"DexWithClasses %s with cost %s puts the linear alloc estimate for the primary dex " +
"at %s, exceeding the maximum of %s.",
dexWithClasses.getPathToDexFile(),
dexWithClasses.getSizeEstimate(),
primaryDexSize,
linearAllocHardLimit);
throw new HumanReadableException("Primary dex exceeds linear alloc limit.");
}
primaryDexContents.add(dexWithClasses);
} else {
// Case 2: Entry must go in a secondary dex.
// If the individual DexWithClasses exceeds the limit for a secondary dex, then we have done
// something horribly wrong.
if (dexWithClasses.getSizeEstimate() > linearAllocHardLimit) {
context.logError(
"DexWithClasses %s with cost %s exceeds the max cost %s for a secondary dex file.",
dexWithClasses.getPathToDexFile(),
dexWithClasses.getSizeEstimate(),
linearAllocHardLimit);
throw new HumanReadableException("Secondary dex exceeds linear alloc limit.");
}
// If there is no current secondary dex, or dexWithClasses would put the current secondary
// dex over the cost threshold, then create a new secondary dex and initialize it with a
// canary.
if (currentSecondaryDexContents == null ||
dexWithClasses.getSizeEstimate() + currentSecondaryDexSize > linearAllocHardLimit) {
DexWithClasses canary = createCanary(secondaryDexesContents.size() + 1, steps);
currentSecondaryDexContents = Lists.newArrayList(canary);
currentSecondaryDexSize = canary.getSizeEstimate();
secondaryDexesContents.add(currentSecondaryDexContents);
}
// Now add the contributions from the dexWithClasses entry.
currentSecondaryDexContents.add(dexWithClasses);
currentSecondaryDexSize += dexWithClasses.getSizeEstimate();
}
}
ImmutableSet<Path> primaryDexInputs = FluentIterable.from(primaryDexContents)
.transform(DexWithClasses.TO_PATH)
.toSet();
Map<Path, DexWithClasses> metadataTxtEntries = Maps.newHashMap();
ImmutableMultimap.Builder<Path, Path> secondaryOutputToInputs = ImmutableMultimap.builder();
for (int index = 0; index < secondaryDexesContents.size(); index++) {
String secondaryDexFilename = dexStore.fileNameForSecondary(index);
Path pathToSecondaryDex = secondaryDexJarFilesDir.resolve(secondaryDexFilename);
metadataTxtEntries.put(pathToSecondaryDex, secondaryDexesContents.get(index).get(0));
Collection<Path> dexContentPaths = Collections2.transform(
secondaryDexesContents.get(index), DexWithClasses.TO_PATH);
secondaryOutputToInputs.putAll(pathToSecondaryDex, dexContentPaths);
}
return new Result(
primaryDexInputs,
secondaryOutputToInputs.build(),
metadataTxtEntries,
getDexInputsHashes(primaryDexContents, secondaryDexesContents));
}
private static ImmutableMap<Path, Sha1HashCode> getDexInputsHashes(
List<DexWithClasses> primaryDexContents,
List<List<DexWithClasses>> secondaryDexesContents) {
Iterable<DexWithClasses> allInputs = Iterables.concat(
primaryDexContents,
Iterables.concat(secondaryDexesContents));
ImmutableMap.Builder<Path, Sha1HashCode> dexInputsHashes = ImmutableMap.builder();
for (DexWithClasses dexWithClasses : allInputs) {
dexInputsHashes.put(dexWithClasses.getPathToDexFile(), dexWithClasses.getClassesHash());
}
return dexInputsHashes.build();
}
private boolean mustBeInPrimaryDex(DexWithClasses dexWithClasses) {
for (String className : dexWithClasses.getClassNames()) {
if (primaryDexFilter.matches(className)) {
return true;
}
}
return false;
}
/**
* @see com.facebook.buck.dalvik.CanaryFactory#create(int)
*/
private DexWithClasses createCanary(final int index, ImmutableList.Builder<Step> steps) {
final FileLike fileLike = CanaryFactory.create(index);
final String canaryDirName = "canary_" + String.valueOf(index);
final Path scratchDirectoryForCanaryClass = scratchDirectory.resolve(canaryDirName);
// Strip the .class suffix to get the class name for the DexWithClasses object.
final String relativePathToClassFile = fileLike.getRelativePath();
Preconditions.checkState(relativePathToClassFile.endsWith(".class"));
final String className = relativePathToClassFile.replaceFirst("\\.class$", "");
// Write out the .class file.
steps.add(new AbstractExecutionStep("write_canary_class") {
@Override
public int execute(ExecutionContext context) {
Path classFile = scratchDirectoryForCanaryClass.resolve(relativePathToClassFile);
ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
try (InputStream inputStream = fileLike.getInput()) {
projectFilesystem.createParentDirs(classFile);
projectFilesystem.copyToPath(inputStream, classFile);
} catch (IOException e) {
context.logError(e, "Error writing canary class file: %s.", classFile.toString());
return 1;
}
return 0;
}
});
return new DexWithClasses() {
@Override
public int getSizeEstimate() {
// Because we do not know the units being used for DEX size estimation and the canary should
// be very small, assume the size is zero.
return 0;
}
@Override
public Path getPathToDexFile() {
return scratchDirectoryForCanaryClass;
}
@Override
public ImmutableSet<String> getClassNames() {
return ImmutableSet.of(className);
}
@Override
public Sha1HashCode getClassesHash() {
// The only thing unique to canary classes is the index, which is captured by canaryDirName.
Hasher hasher = Hashing.sha1().newHasher();
hasher.putString(canaryDirName, Charsets.UTF_8);
return ImmutableSha1HashCode.of(hasher.hash().toString());
}
};
}
public static class Result {
public final Set<Path> primaryDexInputs;
public final Multimap<Path, Path> secondaryOutputToInputs;
public final Map<Path, DexWithClasses> metadataTxtDexEntries;
public final DexInputHashesProvider dexInputHashesProvider;
public Result(
Set<Path> primaryDexInputs,
Multimap<Path, Path> secondaryOutputToInputs,
Map<Path, DexWithClasses> metadataTxtDexEntries,
final ImmutableMap<Path, Sha1HashCode> dexInputHashes) {
this.primaryDexInputs = primaryDexInputs;
this.secondaryOutputToInputs = secondaryOutputToInputs;
this.metadataTxtDexEntries = metadataTxtDexEntries;
this.dexInputHashesProvider = new DexInputHashesProvider() {
@Override
public ImmutableMap<Path, Sha1HashCode> getDexInputHashes() {
return dexInputHashes;
}
};
}
}
}