blob: e40c65bdd4ebdc54d517312f5bc9a21e8b3ff941 [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.sdklib.BuildToolInfo.PathId.ZIP_ALIGN;
import com.android.annotations.NonNull;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.AaptOptions;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext.FileType;
import com.android.build.gradle.internal.scope.ConventionMappingHelper;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantOutputScope;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.BaseTask;
import com.android.build.gradle.internal.variant.ApkVariantOutputData;
import com.android.build.gradle.tasks.PackageApplication;
import com.android.builder.core.AaptPackageProcessBuilder;
import com.android.builder.model.SigningConfig;
import com.android.builder.packaging.DuplicateFileException;
import com.android.builder.packaging.PackagerException;
import com.android.builder.sdk.TargetInfo;
import com.android.ide.common.process.LoggedProcessOutputHandler;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessInfoBuilder;
import com.android.ide.common.signing.KeytoolException;
import com.google.common.collect.Iterables;
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.Optional;
import org.gradle.api.tasks.OutputDirectory;
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.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* Tasks to generate M+ style pure splits APKs with dex files.
*/
public class InstantRunSplitApkBuilder extends BaseTask {
private Set<File> dexFolders;
private File outputDirectory;
private SigningConfig signingConf;
private String applicationId;
private InstantRunBuildContext instantRunBuildContext;
private File zipAlignExe;
private AaptOptions aaptOptions;
private File supportDir;
private ApkVariantOutputData variantOutputData;
@Input
public String getApplicationId() {
return applicationId;
}
public void setApplicationId(String applicationId) {
this.applicationId = applicationId;
}
@Input
public int getVersionCode() {
return variantOutputData.getVersionCode();
}
@Input
@Optional
public String getVersionName() {
return variantOutputData.getVersionName();
}
@InputFiles
public Set<File> getDexFolders() {
return dexFolders;
}
@OutputDirectory
public File getOutputDirectory() {
return outputDirectory;
}
@Input
public File getZipAlignExe() {
return zipAlignExe;
}
@TaskAction
public void run(IncrementalTaskInputs inputs)
throws IOException, DuplicateFileException, KeytoolException, PackagerException,
ProcessException, InterruptedException {
if (inputs.isIncremental()) {
inputs.outOfDate(new Action<InputFileDetails>() {
@Override
public void execute(InputFileDetails inputFileDetails) {
try {
// we generate APKs for all slices but the main slice which will get
// packaged in the main APK.
if (!inputFileDetails.getFile().getName().contains(
InstantRunSlicer.MAIN_SLICE_NAME)) {
generateSplitApk(new DexFile(
inputFileDetails.getFile(),
inputFileDetails.getFile().getParentFile().getName()));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
inputs.removed(new Action<InputFileDetails>() {
@Override
public void execute(InputFileDetails inputFileDetails) {
DexFile dexFile = new DexFile(
inputFileDetails.getFile(),
inputFileDetails.getFile().getParentFile().getName());
String outputFileName = dexFile.encodeName() + "_unaligned.apk";
new File(getOutputDirectory(), outputFileName).delete();
outputFileName = dexFile.encodeName() + ".apk";
new File(getOutputDirectory(), outputFileName).delete();
}
});
} else {
List<DexFile> allFiles = new ArrayList<DexFile>();
for (File dexFolder : getDexFolders()) {
if (dexFolder.isDirectory()) {
File[] files = dexFolder.listFiles();
if (files != null) {
for (File file : files) {
allFiles.add(new DexFile(file, dexFolder.getName()));
}
}
}
}
for (DexFile file : allFiles) {
// generate a split APK for each.
if (!file.dexFile.getParentFile().getName().contains(
InstantRunSlicer.MAIN_SLICE_NAME)) {
generateSplitApk(file);
}
}
}
}
private void generateSplitApk(DexFile file)
throws IOException, DuplicateFileException, KeytoolException, PackagerException,
InterruptedException, ProcessException {
final File outputLocation = new File(getOutputDirectory(), file.encodeName()
+ "_unaligned.apk");
Files.createParentDirs(outputLocation);
File resPackageFile = generateSplitApkManifest(file.encodeName());
getBuilder().packageCodeSplitApk(resPackageFile.getAbsolutePath(),
file.dexFile, signingConf, outputLocation.getAbsolutePath());
// zip align it.
final File alignedOutput = new File(getOutputDirectory(), file.encodeName() + ".apk");
ProcessInfoBuilder processInfoBuilder = new ProcessInfoBuilder();
processInfoBuilder.setExecutable(getZipAlignExe());
processInfoBuilder.addArgs("-f", "4");
processInfoBuilder.addArgs(outputLocation.getAbsolutePath());
processInfoBuilder.addArgs(alignedOutput.getAbsolutePath());
getBuilder().executeProcess(processInfoBuilder.createProcess(),
new LoggedProcessOutputHandler(getILogger()));
instantRunBuildContext.addChangedFile(FileType.SPLIT, alignedOutput);
resPackageFile.delete();
}
// todo, move this to a sub task, as it is reusable between invocations.
private File generateSplitApkManifest(String uniqueName)
throws IOException, ProcessException, InterruptedException {
String versionNameToUse = getVersionName();
if (versionNameToUse == null) {
versionNameToUse = String.valueOf(getVersionCode());
}
File sliceSupportDir = new File(supportDir, uniqueName);
sliceSupportDir.mkdirs();
File androidManifest = new File(sliceSupportDir, "AndroidManifest.xml");
OutputStreamWriter fileWriter = new OutputStreamWriter(
new FileOutputStream(androidManifest), "UTF-8");
try {
fileWriter.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ " package=\"" + getApplicationId() + "\"\n"
+ " android:versionCode=\"" + getVersionCode() + "\"\n"
+ " android:versionName=\"" + versionNameToUse + "\"\n"
+ " split=\"lib_" + uniqueName + "\">\n"
//+ " <uses-sdk android:minSdkVersion=\"21\"/>\n" + "</manifest>\n");
+ "</manifest>\n");
fileWriter.flush();
} finally {
fileWriter.close();
}
File resFilePackageFile = new File(supportDir, "resources_ap");
AaptPackageProcessBuilder aaptPackageCommandBuilder =
new AaptPackageProcessBuilder(androidManifest, getAaptOptions())
.setDebuggable(true)
.setResPackageOutput(resFilePackageFile.getAbsolutePath());
getBuilder().processResources(
aaptPackageCommandBuilder,
false /* enforceUniquePackageName */,
new LoggedProcessOutputHandler(getILogger()));
return resFilePackageFile;
}
public AaptOptions getAaptOptions() {
return aaptOptions;
}
private static class DexFile {
private final File dexFile;
private final String dexFolderName;
private DexFile(@NonNull File dexFile, @NonNull String dexFolderName) {
this.dexFile = dexFile;
this.dexFolderName = dexFolderName;
}
private String encodeName() {
return dexFolderName.replace('-', '_');
}
}
public static class ConfigAction implements TaskConfigAction<InstantRunSplitApkBuilder> {
private final VariantScope variantScope;
public ConfigAction(@NonNull VariantScope variantScope) {
this.variantScope = variantScope;
}
@NonNull
@Override
public String getName() {
return variantScope.getTaskName("instantRun", "PureSplitBuilder");
}
@NonNull
@Override
public Class<InstantRunSplitApkBuilder> getType() {
return InstantRunSplitApkBuilder.class;
}
@Override
public void execute(@NonNull InstantRunSplitApkBuilder task) {
final GradleVariantConfiguration config = variantScope.getVariantConfiguration();
task.outputDirectory = variantScope.getInstantRunSplitApkOutputFolder();
task.signingConf = config.getSigningConfig();
task.setApplicationId(config.getApplicationId());
task.variantOutputData =
(ApkVariantOutputData) variantScope.getVariantData().getOutputs().get(0);
task.setVariantName(
variantScope.getVariantConfiguration().getFullName());
task.setAndroidBuilder(variantScope.getGlobalScope().getAndroidBuilder());
task.instantRunBuildContext = variantScope.getInstantRunBuildContext();
task.supportDir = variantScope.getInstantRunSliceSupportDir();
ConventionMappingHelper.map(task, "zipAlignExe", new Callable<File>() {
@Override
public File call() throws Exception {
final TargetInfo info =
variantScope.getGlobalScope().getAndroidBuilder().getTargetInfo();
if (info == null) {
return null;
}
String path = info.getBuildTools().getPath(ZIP_ALIGN);
if (path == null) {
return null;
}
return new File(path);
}
});
ConventionMappingHelper
.map(task, "dexFolders", new Callable<Set<File>>() {
@Override
public Set<File> call() {
if (config.getUseJack()) {
throw new IllegalStateException(
"InstantRun does not support Jack compiler yet.");
}
return variantScope.getTransformManager()
.getPipelineOutput(PackageApplication.sDexFilter).keySet();
}
});
ConventionMappingHelper.map(task, "aaptOptions",
new Callable<AaptOptions>() {
@Override
public AaptOptions call() throws Exception {
return variantScope.getGlobalScope().getExtension().getAaptOptions();
}
});
}
}
}