| /* |
| * Copyright (C) 2012 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.tasks; |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.build.gradle.internal.scope.ConventionMappingHelper; |
| import com.android.build.gradle.internal.scope.TaskConfigAction; |
| import com.android.build.gradle.internal.scope.VariantScope; |
| import com.android.build.gradle.internal.tasks.BaseTask; |
| import com.android.build.gradle.internal.variant.ApkVariantData; |
| import com.android.build.gradle.internal.variant.TestVariantData; |
| import com.android.build.gradle.internal.PostCompilationData; |
| import com.android.builder.core.AndroidBuilder; |
| import com.android.builder.core.DexOptions; |
| import com.android.builder.core.VariantConfiguration; |
| import com.android.builder.core.VariantType; |
| import com.android.ide.common.internal.LoggedErrorException; |
| import com.android.ide.common.internal.WaitableExecutor; |
| import com.android.ide.common.process.LoggedProcessOutputHandler; |
| import com.android.ide.common.process.ProcessOutputHandler; |
| import com.android.utils.FileUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Lists; |
| 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.Action; |
| import org.gradle.api.tasks.Input; |
| import org.gradle.api.tasks.InputFiles; |
| import org.gradle.api.tasks.Nested; |
| import org.gradle.api.tasks.OutputDirectory; |
| import org.gradle.api.tasks.ParallelizableTask; |
| import org.gradle.api.tasks.TaskAction; |
| import org.gradle.api.tasks.incremental.IncrementalTaskInputs; |
| import org.gradle.api.tasks.incremental.InputFileDetails; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| |
| @ParallelizableTask |
| public class PreDex extends BaseTask { |
| |
| @Input |
| public String getBuildToolsVersion() { |
| return getBuildTools().getRevision().toString(); |
| } |
| |
| private Collection<File> inputFiles; |
| |
| private File outputFolder; |
| |
| private com.android.build.gradle.internal.dsl.DexOptions dexOptions; |
| |
| private boolean multiDex; |
| |
| @TaskAction |
| void taskAction(IncrementalTaskInputs taskInputs) |
| throws IOException, LoggedErrorException, InterruptedException { |
| |
| final boolean multiDexEnabled = isMultiDex(); |
| |
| final File outFolder = getOutputFolder(); |
| |
| boolean incremental = taskInputs.isIncremental(); |
| // if we are not in incremental mode, then outOfDate will contain |
| // all the files, but first we need to delete the previous output |
| if (!incremental) { |
| FileUtils.emptyFolder(outFolder); |
| } |
| |
| final Set<String> hashs = Sets.newHashSet(); |
| final WaitableExecutor<Void> executor = new WaitableExecutor<Void>(); |
| final List<File> inputFileDetails = Lists.newArrayList(); |
| |
| taskInputs.outOfDate(new Action<InputFileDetails>() { |
| @Override |
| public void execute(InputFileDetails change) { |
| inputFileDetails.add(change.getFile()); |
| } |
| }); |
| |
| ProcessOutputHandler outputHandler = new LoggedProcessOutputHandler(getILogger()); |
| for (final File file : inputFileDetails) { |
| Callable<Void> action = new PreDexTask(outFolder, file, hashs, |
| multiDexEnabled, outputHandler); |
| executor.execute(action); |
| } |
| |
| if (incremental) { |
| taskInputs.removed(new Action<InputFileDetails>() { |
| @Override |
| public void execute(InputFileDetails change) { |
| File preDexedFile = getDexFileName(outFolder, change.getFile()); |
| |
| try { |
| FileUtils.deleteFolder(preDexedFile); |
| } catch (IOException e) { |
| getLogger().info("Could not delete {}\n{}", |
| preDexedFile, Throwables.getStackTraceAsString(e)); |
| } |
| } |
| }); |
| } |
| |
| executor.waitForTasksWithQuickFail(false); |
| } |
| |
| private final class PreDexTask implements Callable<Void> { |
| private final File outFolder; |
| private final File fileToProcess; |
| private final Set<String> hashs; |
| private final boolean multiDexEnabled; |
| private final DexOptions options = getDexOptions(); |
| private final AndroidBuilder builder = getBuilder(); |
| private final ProcessOutputHandler mOutputHandler; |
| |
| |
| private PreDexTask( |
| File outFolder, |
| File file, |
| Set<String> hashs, |
| boolean multiDexEnabled, |
| ProcessOutputHandler outputHandler) { |
| this.mOutputHandler = outputHandler; |
| this.outFolder = outFolder; |
| this.fileToProcess = file; |
| this.hashs = hashs; |
| this.multiDexEnabled = multiDexEnabled; |
| } |
| |
| @Override |
| public Void call() throws Exception { |
| // TODO remove once we can properly add a library as a dependency of its test. |
| String hash = getFileHash(fileToProcess); |
| |
| synchronized (hashs) { |
| if (hashs.contains(hash)) { |
| return null; |
| } |
| |
| hashs.add(hash); |
| } |
| |
| File preDexedFile = getDexFileName(outFolder, fileToProcess); |
| |
| if (multiDexEnabled) { |
| preDexedFile.mkdirs(); |
| } |
| |
| builder.preDexLibrary( |
| fileToProcess, preDexedFile, multiDexEnabled, options, mOutputHandler); |
| |
| return null; |
| } |
| } |
| |
| // this is used automatically by Gradle, even though nothing |
| // in the class uses it. |
| @SuppressWarnings("unused") |
| @InputFiles |
| public Collection<File> getInputFiles() { |
| return inputFiles; |
| } |
| |
| public void setInputFiles(Collection<File> inputFiles) { |
| this.inputFiles = inputFiles; |
| } |
| |
| @OutputDirectory |
| public File getOutputFolder() { |
| return outputFolder; |
| } |
| |
| public void setOutputFolder(File outputFolder) { |
| this.outputFolder = outputFolder; |
| } |
| |
| @Nested |
| public com.android.build.gradle.internal.dsl.DexOptions getDexOptions() { |
| return dexOptions; |
| } |
| |
| public void setDexOptions(com.android.build.gradle.internal.dsl.DexOptions dexOptions) { |
| this.dexOptions = dexOptions; |
| } |
| |
| @Input |
| public boolean isMultiDex() { |
| return multiDex; |
| } |
| |
| public void setMultiDex(boolean multiDex) { |
| this.multiDex = multiDex; |
| } |
| |
| /** |
| * Returns the hash of a file. |
| * @param file the file to hash |
| */ |
| private static String getFileHash(@NonNull File file) throws IOException { |
| HashCode hashCode = Files.hash(file, Hashing.sha1()); |
| return hashCode.toString(); |
| } |
| |
| /** |
| * Returns a unique File for the pre-dexed library, even |
| * if there are 2 libraries with the same file names (but different |
| * paths) |
| * |
| * If multidex is enabled the return File is actually a folder. |
| * |
| * @param outFolder the output folder. |
| * @param inputFile the library. |
| */ |
| @NonNull |
| static File getDexFileName(@NonNull File outFolder, @NonNull File inputFile) { |
| // get the filename |
| String name = inputFile.getName(); |
| // remove the extension |
| int pos = name.lastIndexOf('.'); |
| if (pos != -1) { |
| name = name.substring(0, pos); |
| } |
| |
| // add a hash of the original file path. |
| String input = inputFile.getAbsolutePath(); |
| HashFunction hashFunction = Hashing.sha1(); |
| HashCode hashCode = hashFunction.hashString(input, Charsets.UTF_16LE); |
| |
| return new File(outFolder, name + "-" + hashCode.toString() + SdkConstants.DOT_JAR); |
| } |
| |
| public static class ConfigAction implements TaskConfigAction<PreDex> { |
| |
| private VariantScope scope; |
| |
| private Callable<List<File>> inputLibraries; |
| |
| public ConfigAction(VariantScope scope, PostCompilationData pcData) { |
| this.scope = scope; |
| this.inputLibraries = pcData.getInputLibrariesCallable(); |
| } |
| |
| @Override |
| public String getName() { |
| return scope.getTaskName("preDex"); |
| } |
| |
| @Override |
| public Class<PreDex> getType() { |
| return PreDex.class; |
| } |
| |
| @Override |
| public void execute(PreDex preDexTask) { |
| ApkVariantData variantData = (ApkVariantData) scope.getVariantData(); |
| VariantConfiguration config = variantData.getVariantConfiguration(); |
| |
| boolean isTestForApp = config.getType().isForTesting() && |
| ((TestVariantData) variantData).getTestedVariantData() |
| .getVariantConfiguration().getType() == VariantType.DEFAULT; |
| boolean isMultiDexEnabled = config.isMultiDexEnabled() && !isTestForApp; |
| |
| variantData.preDexTask = preDexTask; |
| preDexTask.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder()); |
| preDexTask.setVariantName(config.getFullName()); |
| preDexTask.dexOptions = scope.getGlobalScope().getExtension().getDexOptions(); |
| preDexTask.multiDex = isMultiDexEnabled; |
| |
| ConventionMappingHelper.map(preDexTask, "inputFiles", inputLibraries); |
| ConventionMappingHelper.map(preDexTask, "outputFolder", new Callable<File>() { |
| @Override |
| public File call() throws Exception { |
| return scope.getPreDexOutputDir(); |
| } |
| }); |
| } |
| } |
| } |