blob: 05482feb29da81dc846bb2be03769b8f28e6212d [file] [log] [blame]
/*
* Copyright (C) 2015 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.gradle.internal.transforms;
import static com.android.utils.FileUtils.mkdirs;
import static com.google.common.base.Preconditions.checkNotNull;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.gradle.internal.LoggerWrapper;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext.FileType;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.builder.core.AndroidBuilder;
import com.android.builder.core.DexOptions;
import com.android.builder.sdk.TargetInfo;
import com.android.ide.common.blame.Message;
import com.android.ide.common.blame.ParsingProcessOutputHandler;
import com.android.ide.common.blame.parser.DexParser;
import com.android.ide.common.blame.parser.ToolOutputParser;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessOutputHandler;
import com.android.sdklib.BuildToolInfo;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import org.gradle.api.logging.Logger;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* Dexing as a transform.
*
* This consumes all the available classes streams and creates a dex file (or more in the case of
* multi-dex)
*
* This handles pre-dexing as well. If there are more than one stream, then only streams with
* changed files will be re-dexed before a single merge phase is done at the end.
* If there is a single input, then there's only a single dx phase.
*/
public class DexTransform extends Transform {
@NonNull
private final DexOptions dexOptions;
private final boolean debugMode;
private final boolean multiDex;
@NonNull
private final File intermediateFolder;
@Nullable
private final File mainDexListFile;
@NonNull
private final AndroidBuilder androidBuilder;
@NonNull
private final ILogger logger;
private final InstantRunBuildContext instantRunBuildContext;
public DexTransform(
@NonNull DexOptions dexOptions,
boolean debugMode,
boolean multiDex,
@Nullable File mainDexListFile,
@NonNull File intermediateFolder,
@NonNull AndroidBuilder androidBuilder,
@NonNull Logger logger,
@NonNull InstantRunBuildContext instantRunBuildContext) {
this.dexOptions = dexOptions;
this.debugMode = debugMode;
this.multiDex = multiDex;
this.mainDexListFile = mainDexListFile;
this.intermediateFolder = intermediateFolder;
this.androidBuilder = androidBuilder;
this.logger = new LoggerWrapper(logger);
this.instantRunBuildContext = instantRunBuildContext;
}
@NonNull
@Override
public String getName() {
return "dex";
}
@NonNull
@Override
public Set<ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@NonNull
@Override
public Set<ContentType> getOutputTypes() {
return TransformManager.CONTENT_DEX;
}
@NonNull
@Override
public Set<Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@NonNull
@Override
public Collection<File> getSecondaryFileInputs() {
if (mainDexListFile != null) {
return ImmutableList.of(mainDexListFile);
}
return ImmutableList.of();
}
@NonNull
@Override
public Collection<File> getSecondaryDirectoryOutputs() {
// we use the intermediate folder only if
// - there's per-scope dexing
// - there's no native multi-dex
if (dexOptions.getPreDexLibraries() && !(multiDex && mainDexListFile == null)) {
return ImmutableList.of(intermediateFolder);
}
return ImmutableList.of();
}
@NonNull
@Override
public Map<String, Object> getParameterInputs() {
try {
Map<String, Object> params = Maps.newHashMapWithExpectedSize(4);
params.put("debugMode", debugMode);
params.put("predex", dexOptions.getPreDexLibraries());
params.put("incremental", dexOptions.getIncremental());
params.put("jumbo", dexOptions.getJumboMode());
params.put("multidex", multiDex);
params.put("multidex-legacy", multiDex && mainDexListFile != null);
TargetInfo targetInfo = androidBuilder.getTargetInfo();
Preconditions.checkState(targetInfo != null,
"androidBuilder.targetInfo required for task '%s'.", getName());
BuildToolInfo buildTools = targetInfo.getBuildTools();
params.put("build-tools", buildTools.getRevision().toString());
return params;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(TransformInvocation transformInvocation)
throws TransformException, IOException, InterruptedException {
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
boolean isIncremental = transformInvocation.isIncremental();
checkNotNull(outputProvider, "Missing output object for transform " + getName());
// Gather a full list of all inputs.
List<JarInput> jarInputs = Lists.newArrayList();
List<DirectoryInput> directoryInputs = Lists.newArrayList();
for (TransformInput input : transformInvocation.getInputs()) {
jarInputs.addAll(input.getJarInputs());
directoryInputs.addAll(input.getDirectoryInputs());
}
ProcessOutputHandler outputHandler = new ParsingProcessOutputHandler(
new ToolOutputParser(new DexParser(), Message.Kind.ERROR, logger),
new ToolOutputParser(new DexParser(), logger),
androidBuilder.getErrorReporter());
if (!isIncremental) {
outputProvider.deleteAll();
}
try {
// if only one scope or no per-scope dexing, just do a single pass that
// runs dx on everything.
if ((jarInputs.size() + directoryInputs.size()) == 1 || !dexOptions.getPreDexLibraries()) {
File outputDir = outputProvider.getContentLocation("main",
getOutputTypes(), getScopes(),
Format.DIRECTORY);
FileUtils.mkdirs(outputDir);
// first delete the output folder where the final dex file(s) will be.
FileUtils.emptyFolder(outputDir);
// gather the inputs. This mode is always non incremental, so just
// gather the top level folders/jars
final List<File> inputFiles = Lists.newArrayList();
for (JarInput jarInput : jarInputs) {
inputFiles.add(jarInput.getFile());
}
for (DirectoryInput directoryInput : directoryInputs) {
inputFiles.add(directoryInput.getFile());
}
androidBuilder.convertByteCode(
inputFiles,
outputDir,
multiDex,
mainDexListFile,
dexOptions,
null,
false,
true,
outputHandler,
false /* instantRunMode */);
for (File file : Files.fileTreeTraverser().breadthFirstTraversal(outputDir)) {
if (file.isFile()) {
instantRunBuildContext.addChangedFile(FileType.DEX, file);
}
}
} else {
// Figure out if we need to do a dx merge.
// The ony case we don't need it is in native multi-dex mode when doing debug
// builds. This saves build time at the expense of too many dex files which is fine.
// FIXME dx cannot receive dex files to merge inside a folder. They have to be in a jar. Need to fix in dx.
boolean needMerge = !multiDex || mainDexListFile != null;// || !debugMode;
// where we write the pre-dex depends on whether we do the merge after.
// If needMerge changed from one build to another, we'll be in non incremental
// mode, so we don't have to deal with changing folder in incremental mode.
File perStreamDexFolder = null;
if (needMerge) {
perStreamDexFolder = intermediateFolder;
if (!isIncremental) {
FileUtils.deleteFolder(perStreamDexFolder);
}
}
// dex all the different streams separately, then merge later (maybe)
// hash to detect duplicate jars (due to isse with library and tests)
final Set<String> hashs = Sets.newHashSet();
// input files to output file map
final Map<File, File> inputFiles = Maps.newHashMap();
// stuff to delete. Might be folders.
final List<File> deletedFiles = Lists.newArrayList();
// first gather the different inputs to be dexed separately.
for (DirectoryInput directoryInput : directoryInputs) {
File rootFolder = directoryInput.getFile();
// The incremental mode only detect file level changes.
// It does not handle removed root folders. However the transform
// task will add the TransformInput right after it's removed so that it
// can be detected by the transform.
if (!rootFolder.exists()) {
// if the root folder is gone we need to remove the previous
// output
File preDexedFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
directoryInput);
if (preDexedFile.exists()) {
deletedFiles.add(preDexedFile);
}
} else if (!isIncremental || !directoryInput.getChangedFiles().isEmpty()) {
// add the folder for re-dexing only if we're not in incremental
// mode or if it contains changed files.
File preDexFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
directoryInput);
inputFiles.put(rootFolder, preDexFile);
}
}
for (JarInput jarInput : jarInputs) {
switch (jarInput.getStatus()) {
case NOTCHANGED:
if (isIncremental) {
break;
}
// intended fall-through
case CHANGED:
case ADDED: {
File preDexFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
jarInput);
inputFiles.put(jarInput.getFile(), preDexFile);
break;
}
case REMOVED: {
File preDexedFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
jarInput);
if (preDexedFile.exists()) {
deletedFiles.add(preDexedFile);
}
break;
}
}
}
WaitableExecutor<Void> executor = new WaitableExecutor<Void>();
for (Map.Entry<File, File> entry : inputFiles.entrySet()) {
Callable<Void> action = new PreDexTask(
entry.getKey(),
entry.getValue(),
hashs,
outputHandler);
executor.execute(action);
}
for (final File file : deletedFiles) {
executor.execute(new Callable<Void>() {
@Override
public Void call() throws Exception {
FileUtils.deleteFolder(file);
return null;
}
});
}
executor.waitForTasksWithQuickFail(false);
if (needMerge) {
File outputDir = outputProvider.getContentLocation("main",
TransformManager.CONTENT_DEX, getScopes(),
Format.DIRECTORY);
FileUtils.mkdirs(outputDir);
// first delete the output folder where the final dex file(s) will be.
FileUtils.emptyFolder(outputDir);
mkdirs(outputDir);
// find the inputs of the dex merge.
// they are the content of the intermediate folder.
List<File> outputs = null;
if (!multiDex) {
// content of the folder is jar files.
File[] files = intermediateFolder.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String name) {
return name.endsWith(SdkConstants.DOT_JAR);
}
});
if (files != null) {
outputs = Arrays.asList(files);
}
} else {
File[] directories = intermediateFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if (directories != null) {
outputs = Arrays.asList(directories);
}
}
if (outputs == null) {
throw new RuntimeException("No dex files to merge!");
}
androidBuilder.convertByteCode(
outputs,
outputDir,
multiDex,
mainDexListFile,
dexOptions,
null,
false,
true,
outputHandler,
false /* instantRunMode */);
}
}
} catch (LoggedErrorException e) {
throw new TransformException(e);
} catch (ProcessException e) {
throw new TransformException(e);
} catch (Exception e) {
throw new TransformException(e);
}
}
private final class PreDexTask implements Callable<Void> {
@NonNull
private final File from;
@NonNull
private final File to;
@NonNull
private final Set<String> hashs;
@NonNull
private final ProcessOutputHandler mOutputHandler;
private PreDexTask(
@NonNull File from,
@NonNull File to,
@NonNull Set<String> hashs,
@NonNull ProcessOutputHandler outputHandler) {
this.from = from;
this.to = to;
this.hashs = hashs;
this.mOutputHandler = outputHandler;
}
@Override
public Void call() throws Exception {
// TODO remove once we can properly add a library as a dependency of its test.
String hash = getFileHash(from);
synchronized (hashs) {
if (hashs.contains(hash)) {
return null;
}
hashs.add(hash);
}
if (to.isDirectory()) {
FileUtils.emptyFolder(to);
} else if (to.isFile()) {
FileUtils.delete(to);
} else {
if (multiDex) {
mkdirs(to);
} else {
mkdirs(to.getParentFile());
}
}
androidBuilder.preDexLibrary(
from, to, multiDex, dexOptions, mOutputHandler);
for (File file : Files.fileTreeTraverser().breadthFirstTraversal(to)) {
if (file.isFile()) {
instantRunBuildContext.addChangedFile(FileType.DEX, file);
}
}
return null;
}
}
/**
* Returns the hash of a file.
*
* If the file is a folder, it's a hash of its path. If the file is a file, then
* it's a hash of the file itself.
*
* @param file the file to hash
*/
@NonNull
private static String getFileHash(@NonNull File file) throws IOException {
HashCode hashCode;
HashFunction hashFunction = Hashing.sha1();
if (file.isDirectory()) {
hashCode = hashFunction.hashString(file.getPath(), Charsets.UTF_16LE);
} else {
hashCode = Files.hash(file, hashFunction);
}
return hashCode.toString();
}
@NonNull
private File getPreDexFile(
@NonNull TransformOutputProvider output,
boolean needMerge,
@Nullable File outFolder,
@NonNull QualifiedContent qualifiedContent) {
if (needMerge) {
checkNotNull(outFolder);
return new File(outFolder, getFilename(qualifiedContent.getFile()));
} else {
return getOutputLocation(output, qualifiedContent, qualifiedContent.getFile());
}
}
@NonNull
private File getOutputLocation(
@NonNull TransformOutputProvider output,
@NonNull QualifiedContent qualifiedContent,
@NonNull File file) {
// In InstantRun mode, all files are guaranteed to have a unique name due to the slicer
// transform. adding sha1 to the name can lead to cleaning issues in device, it's much
// easier if the slices always have the same names, irrespective of the current vairiant,
// last version wins.
String name = instantRunBuildContext.isInInstantRunMode()
&& (qualifiedContent.getScopes().contains(Scope.PROJECT)
|| qualifiedContent.getScopes().contains(Scope.SUB_PROJECTS))
? file.getName() : getFilename(file);
File contentLocation = output.getContentLocation(name,
TransformManager.CONTENT_DEX, qualifiedContent.getScopes(),
multiDex ? Format.DIRECTORY : Format.JAR);
if (multiDex) {
FileUtils.mkdirs(contentLocation);
} else {
FileUtils.mkdirs(contentLocation.getParentFile());
}
return contentLocation;
}
@NonNull
private String getFilename(@NonNull File inputFile) {
// If multidex is enabled, this name will be used for a folder and classes*.dex files will
// inside of it.
String suffix = multiDex ? "" : SdkConstants.DOT_JAR;
return FileUtils.getDirectoryNameForJar(inputFile) + suffix;
}
}