com.android.build.gradle.tasks.PackageAndroidArtifact Maven / Gradle / Ivy
Show all versions of gradle-core Show documentation
/*
* Copyright (C) 2016 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 static com.google.common.base.Preconditions.checkNotNull;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.apkzlib.utils.CachedFileContents;
import com.android.apkzlib.utils.IOExceptionWrapper;
import com.android.apkzlib.zip.compress.Zip64NotSupportedException;
import com.android.build.gradle.internal.annotations.PackageFile;
import com.android.build.gradle.internal.dsl.AbiSplitOptions;
import com.android.build.gradle.internal.dsl.CoreSigningConfig;
import com.android.build.gradle.internal.dsl.PackagingOptions;
import com.android.build.gradle.internal.incremental.DexPackagingPolicy;
import com.android.build.gradle.internal.incremental.FileType;
import com.android.build.gradle.internal.incremental.InstantRunBuildContext;
import com.android.build.gradle.internal.incremental.InstantRunPatchingPolicy;
import com.android.build.gradle.internal.packaging.ApkCreatorFactories;
import com.android.build.gradle.internal.scope.ConventionMappingHelper;
import com.android.build.gradle.internal.scope.PackagingScope;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.tasks.FileSupplier;
import com.android.build.gradle.internal.tasks.IncrementalTask;
import com.android.build.gradle.internal.transforms.InstantRunSlicer;
import com.android.build.gradle.internal.variant.SplitHandlingPolicy;
import com.android.builder.files.FileCacheByPath;
import com.android.builder.files.IncrementalRelativeFileSets;
import com.android.builder.files.RelativeFile;
import com.android.builder.internal.packaging.IncrementalPackager;
import com.android.builder.model.AaptOptions;
import com.android.builder.model.ApiVersion;
import com.android.apkzlib.zfile.ApkCreatorFactory;
import com.android.builder.packaging.PackagerException;
import com.android.builder.packaging.PackagingUtils;
import com.android.ide.common.res2.FileStatus;
import com.android.ide.common.signing.CertificateInfo;
import com.android.ide.common.signing.KeystoreHelper;
import com.android.ide.common.signing.KeytoolException;
import com.android.utils.FileUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicates;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.collect.Sets;
import com.google.common.io.Closer;
import com.google.common.io.Files;
import java.io.BufferedInputStream;
import org.gradle.api.Task;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.tooling.BuildException;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Abstract task to package an Android artifact.
*/
public abstract class PackageAndroidArtifact extends IncrementalTask implements FileSupplier {
public static final String INSTANT_RUN_PACKAGES_PREFIX = "instant-run";
// ----- PUBLIC TASK API -----
@InputFile
public File getResourceFile() {
return resourceFile;
}
public void setResourceFile(File resourceFile) {
this.resourceFile = resourceFile;
}
@OutputFile
public File getOutputFile() {
return outputFile;
}
public void setOutputFile(File outputFile) {
this.outputFile = outputFile;
}
@Input
public Set getAbiFilters() {
return abiFilters;
}
public void setAbiFilters(Set abiFilters) {
this.abiFilters = abiFilters;
}
// ----- PRIVATE TASK API -----
@InputFiles
@Optional
public Collection getJavaResourceFiles() {
return javaResourceFiles;
}
@InputFiles
@Optional
public Collection getJniFolders() {
return jniFolders;
}
private File resourceFile;
private Set dexFolders;
private File assets;
private File atomMetadataFolder;
@InputFiles
@Optional
public Set getDexFolders() {
return dexFolders;
}
public void setDexFolders(Set dexFolders) {
this.dexFolders = dexFolders;
}
@InputDirectory
public File getAssets() {
return assets;
}
public void setAssets(File assets) {
this.assets = assets;
}
@InputDirectory
@Optional
public File getAtomMetadataFolder() {
return atomMetadataFolder;
}
/** list of folders and/or jars that contain the merged java resources. */
private Set javaResourceFiles;
private Set jniFolders;
@PackageFile
private File outputFile;
private Set abiFilters;
private boolean debugBuild;
private boolean jniDebugBuild;
private CoreSigningConfig signingConfig;
private PackagingOptions packagingOptions;
private ApiVersion minSdkVersion;
protected InstantRunBuildContext instantRunContext;
protected File instantRunSupportDir;
protected DexPackagingPolicy dexPackagingPolicy;
protected File manifest;
protected AaptOptions aaptOptions;
protected FileType instantRunFileType;
/**
* Name of directory, inside the intermediate directory, where zip caches are kept.
*/
private static final String ZIP_DIFF_CACHE_DIR = "zip-cache";
private static final String ZIP_64_COPY_DIR = "zip64-copy";
/**
* Zip caches to allow incremental updates.
*/
protected FileCacheByPath cacheByPath;
@Input
public boolean getJniDebugBuild() {
return jniDebugBuild;
}
public void setJniDebugBuild(boolean jniDebugBuild) {
this.jniDebugBuild = jniDebugBuild;
}
@Input
public boolean getDebugBuild() {
return debugBuild;
}
public void setDebugBuild(boolean debugBuild) {
this.debugBuild = debugBuild;
}
@Nested
@Optional
public CoreSigningConfig getSigningConfig() {
return signingConfig;
}
public void setSigningConfig(CoreSigningConfig signingConfig) {
this.signingConfig = signingConfig;
}
@Nested
public PackagingOptions getPackagingOptions() {
return packagingOptions;
}
public void setPackagingOptions(PackagingOptions packagingOptions) {
this.packagingOptions = packagingOptions;
}
@Input
public int getMinSdkVersion() {
return this.minSdkVersion.getApiLevel();
}
public void setMinSdkVersion(ApiVersion version) {
this.minSdkVersion = version;
}
@Input
public String getDexPackagingPolicy() {
return dexPackagingPolicy.toString();
}
/*
* We don't really use this. But this forces a full build if the native packaging mode changes.
*/
@Input
public String getNativeLibrariesPackagingModeName() {
return PackagingUtils.getNativeLibrariesLibrariesPackagingMode(manifest).toString();
}
@Input
public Collection getNoCompressExtensions() {
return MoreObjects.>firstNonNull(
aaptOptions.getNoCompress(), Collections.emptyList());
}
protected Predicate getNoCompressPredicate() {
return PackagingUtils.getNoCompressPredicate(aaptOptions, manifest);
}
@Override
protected void doFullTaskAction() throws IOException {
/*
* Clear the cache to make sure we have do not do an incremental build.
*/
cacheByPath.clear();
/*
* Also clear the intermediate build directory. We don't know if anything is in there and
* since this is a full build, we don't want to get any interference from previous state.
*/
FileUtils.deleteDirectoryContents(getIncrementalFolder());
Set androidResources = new HashSet<>();
File androidResourceFile = getResourceFile();
if (androidResourceFile != null) {
androidResources.add(androidResourceFile);
}
/*
* Additionally, make sure we have no previous package, if it exists.
*/
getOutputFile().delete();
ImmutableMap updatedDex =
IncrementalRelativeFileSets.fromZipsAndDirectories(getDexFolders());
ImmutableMap.Builder updatedJavaResourcesBuilder =
ImmutableMap.builder();
for (File javaResourceFile : getJavaResourceFiles()) {
try {
updatedJavaResourcesBuilder.putAll(
javaResourceFile.isFile()
? IncrementalRelativeFileSets.fromZip(javaResourceFile)
: IncrementalRelativeFileSets.fromDirectory(javaResourceFile));
} catch (Zip64NotSupportedException e) {
updatedJavaResourcesBuilder.putAll(
IncrementalRelativeFileSets.fromZip(
copyJavaResourcesOnly(getIncrementalFolder(), javaResourceFile)));
}
}
ImmutableMap updatedJavaResources =
updatedJavaResourcesBuilder.build();
ImmutableMap updatedAssets =
IncrementalRelativeFileSets.fromZipsAndDirectories(
Collections.singleton(getAssets()));
ImmutableMap updatedAndroidResources =
IncrementalRelativeFileSets.fromZipsAndDirectories(androidResources);
ImmutableMap updatedJniResources =
IncrementalRelativeFileSets.fromZipsAndDirectories(getJniFolders());
ImmutableMap updatedAtomMetadata;
if (getAtomMetadataFolder() == null) {
updatedAtomMetadata = ImmutableMap.of();
} else {
updatedAtomMetadata =
IncrementalRelativeFileSets.fromDirectory(getAtomMetadataFolder());
}
doTask(
updatedDex,
updatedJavaResources,
updatedAssets,
updatedAndroidResources,
updatedJniResources,
updatedAtomMetadata);
/*
* Update the known files.
*/
KnownFilesSaveData saveData = KnownFilesSaveData.make(getIncrementalFolder());
saveData.setInputSet(updatedDex.keySet(), InputSet.DEX);
saveData.setInputSet(updatedJavaResources.keySet(), InputSet.JAVA_RESOURCE);
saveData.setInputSet(updatedAssets.keySet(), InputSet.ASSET);
saveData.setInputSet(updatedAndroidResources.keySet(), InputSet.ANDROID_RESOURCE);
saveData.setInputSet(updatedJniResources.keySet(), InputSet.NATIVE_RESOURCE);
saveData.setInputSet(updatedAtomMetadata.keySet(), InputSet.ATOM_METADATA);
saveData.saveCurrentData();
}
/**
* Copy the input zip file (probably a Zip64) content into a new Zip in the destination folder
* stripping out all .class files.
*
* @param destinationFolder the destination folder to use, the output jar will have the same
* name as the input zip file.
* @param zip64File the input zip file.
* @return the path to the stripped Zip file.
* @throws IOException if the copying failed.
*/
@VisibleForTesting
static File copyJavaResourcesOnly(File destinationFolder, File zip64File) throws IOException {
File cacheDir = new File(destinationFolder, ZIP_64_COPY_DIR);
File copiedZip = new File(cacheDir, zip64File.getName());
FileUtils.mkdirs(copiedZip.getParentFile());
try (ZipFile inFile = new ZipFile(zip64File);
ZipOutputStream outFile =
new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(copiedZip)))) {
Enumeration extends ZipEntry> entries = inFile.entries();
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
if (!zipEntry.getName().endsWith(SdkConstants.DOT_CLASS)) {
outFile.putNextEntry(new ZipEntry(zipEntry.getName()));
try {
ByteStreams.copy(
new BufferedInputStream(inFile.getInputStream(zipEntry)), outFile);
} finally {
outFile.closeEntry();
}
}
}
}
return copiedZip;
}
/**
* Packages the application incrementally. In case of instant run packaging, this is not a
* perfectly incremental task as some files are always rewritten even if no change has
* occurred.
*
* @param changedDex incremental dex packaging data
* @param changedJavaResources incremental java resources
* @param changedAssets incremental assets
* @param changedAndroidResources incremental Android resource
* @param changedNLibs incremental native libraries changed
* @param changedAtomMetadata incremental atom metadata changed
* @throws IOException failed to package the APK
*/
private void doTask(
@NonNull ImmutableMap changedDex,
@NonNull ImmutableMap changedJavaResources,
@NonNull ImmutableMap changedAssets,
@NonNull ImmutableMap changedAndroidResources,
@NonNull ImmutableMap changedNLibs,
@NonNull ImmutableMap changedAtomMetadata)
throws IOException {
ImmutableMap.Builder javaResourcesForApk =
ImmutableMap.builder();
javaResourcesForApk.putAll(changedJavaResources);
Collection instantRunDexBaseFiles;
switch(dexPackagingPolicy) {
case INSTANT_RUN_SHARDS_IN_SINGLE_APK:
/*
* If we're doing instant run, then we don't want to treat all dex archives
* as dex archives for packaging. We will package some of the dex files as
* resources.
*
* All dex files in directories whose name contains INSTANT_RUN_PACKAGES_PREFIX
* are kept in the apk as dex files. All other dex files are placed as
* resources as defined by makeInstantRunResourcesFromDex.
*/
instantRunDexBaseFiles = getDexFolders()
.stream()
.filter(input -> input.getName().contains(INSTANT_RUN_PACKAGES_PREFIX))
.collect(Collectors.toSet());
Iterable nonInstantRunDexBaseFiles = getDexFolders()
.stream()
.filter(f -> !instantRunDexBaseFiles.contains(f))
.collect(Collectors.toSet());
ImmutableMap newInstantRunResources =
makeInstantRunResourcesFromDex(nonInstantRunDexBaseFiles);
@SuppressWarnings("unchecked")
ImmutableMap updatedChangedResources =
IncrementalRelativeFileSets.union(
Sets.newHashSet(changedJavaResources, newInstantRunResources));
changedJavaResources = updatedChangedResources;
changedDex = ImmutableMap.copyOf(
Maps.filterKeys(
changedDex,
Predicates.compose(
Predicates.in(instantRunDexBaseFiles),
RelativeFile::getBase
)));
break;
case INSTANT_RUN_MULTI_APK:
changedDex = ImmutableMap.copyOf(
Maps.filterKeys(
changedDex,
Predicates.compose(
Predicates.in(getDexFolders()),
RelativeFile::getBase
)));
case STANDARD:
break;
default:
throw new RuntimeException(
"Unhandled DexPackagingPolicy : " + getDexPackagingPolicy());
}
PrivateKey key;
X509Certificate certificate;
boolean v1SigningEnabled;
boolean v2SigningEnabled;
try {
if (signingConfig != null && signingConfig.isSigningReady()) {
CertificateInfo certificateInfo =
KeystoreHelper.getCertificateInfo(
signingConfig.getStoreType(),
checkNotNull(signingConfig.getStoreFile()),
checkNotNull(signingConfig.getStorePassword()),
checkNotNull(signingConfig.getKeyPassword()),
checkNotNull(signingConfig.getKeyAlias()));
key = certificateInfo.getKey();
certificate = certificateInfo.getCertificate();
v1SigningEnabled = signingConfig.isV1SigningEnabled();
v2SigningEnabled = signingConfig.isV2SigningEnabled();
} else {
key = null;
certificate = null;
v1SigningEnabled = false;
v2SigningEnabled = false;
}
ApkCreatorFactory.CreationData creationData =
new ApkCreatorFactory.CreationData(
getOutputFile(),
key,
certificate,
v1SigningEnabled,
v2SigningEnabled,
null, // BuiltBy
getBuilder().getCreatedBy(),
getMinSdkVersion(),
PackagingUtils.getNativeLibrariesLibrariesPackagingMode(manifest),
getNoCompressPredicate());
getLogger().debug(
"Information to create the APK: apkPath={}, v1SigningEnabled={},"
+ " v2SigningEnabled={}, builtBy={}, createdBy={}, minSdkVersion={},"
+ " nativeLibrariesPackagingMode={}",
creationData.getApkPath(),
creationData.isV1SigningEnabled(),
creationData.isV2SigningEnabled(),
creationData.getBuiltBy(),
creationData.getCreatedBy(),
creationData.getMinSdkVersion(),
creationData.getNativeLibrariesPackagingMode());
try (IncrementalPackager packager = createPackager(creationData)) {
packager.updateDex(changedDex);
packager.updateJavaResources(changedJavaResources);
packager.updateAssets(changedAssets);
packager.updateAndroidResources(changedAndroidResources);
packager.updateNativeLibraries(changedNLibs);
packager.updateAtomMetadata(changedAtomMetadata);
}
} catch (PackagerException | KeytoolException e) {
throw new RuntimeException(e);
}
/*
* Save all used zips in the cache.
*/
Stream.concat(
changedDex.keySet().stream(),
Stream.concat(
changedJavaResources.keySet().stream(),
Stream.concat(
changedAndroidResources.keySet().stream(),
changedNLibs.keySet().stream())))
.map(RelativeFile::getBase)
.filter(File::isFile)
.distinct()
.forEach((File f) -> {
try {
cacheByPath.add(f);
} catch (IOException e) {
throw new IOExceptionWrapper(e);
}
});
// Mark this APK production, this will eventually be saved when instant-run is enabled.
// this might get overridden if the apk is signed/aligned.
try {
instantRunContext.addChangedFile(instantRunFileType, getOutputFile());
} catch (IOException e) {
throw new BuildException(e.getMessage(), e);
}
}
@NonNull
private IncrementalPackager createPackager(ApkCreatorFactory.CreationData creationData)
throws PackagerException, IOException {
return new IncrementalPackager(
creationData,
getIncrementalFolder(),
ApkCreatorFactories.fromProjectProperties(getProject(), getDebugBuild()),
getAbiFilters(),
getJniDebugBuild());
}
@Override
protected boolean isIncremental() {
return true;
}
@Override
protected void doIncrementalTaskAction(Map changedInputs) throws IOException {
checkNotNull(changedInputs, "changedInputs == null");
Set androidResources = new HashSet<>();
File androidResourceFile = getResourceFile();
if (androidResourceFile != null) {
androidResources.add(androidResourceFile);
}
KnownFilesSaveData saveData = KnownFilesSaveData.make(getIncrementalFolder());
Set cacheUpdates = new HashSet<>();
ImmutableMap changedDexFiles =
getChangedInputs(
changedInputs,
saveData,
InputSet.DEX,
getDexFolders(),
cacheByPath,
cacheUpdates);
ImmutableMap changedJavaResources =
getChangedInputs(
changedInputs,
saveData,
InputSet.JAVA_RESOURCE,
getJavaResourceFiles(),
cacheByPath,
cacheUpdates);
ImmutableMap changedAssets =
getChangedInputs(
changedInputs,
saveData,
InputSet.ASSET,
Collections.singleton(getAssets()),
cacheByPath,
cacheUpdates);
ImmutableMap changedAndroidResources =
getChangedInputs(
changedInputs,
saveData,
InputSet.ANDROID_RESOURCE,
androidResources,
cacheByPath,
cacheUpdates);
ImmutableMap changedNLibs =
getChangedInputs(
changedInputs,
saveData,
InputSet.NATIVE_RESOURCE,
getJniFolders(),
cacheByPath,
cacheUpdates);
ImmutableMap changedAtomMetadata;
if (getAtomMetadataFolder() == null) {
changedAtomMetadata = ImmutableMap.of();
} else {
changedAtomMetadata = getChangedInputs(
changedInputs,
saveData,
InputSet.ATOM_METADATA,
ImmutableList.of(getAtomMetadataFolder()),
cacheByPath,
cacheUpdates);
}
doTask(
changedDexFiles,
changedJavaResources,
changedAssets,
changedAndroidResources,
changedNLibs,
changedAtomMetadata);
/*
* Update the cache
*/
cacheUpdates.forEach(Runnable::run);
/*
* Update the save data keep files.
*/
ImmutableMap allDex =
IncrementalRelativeFileSets.fromZipsAndDirectories(getDexFolders());
ImmutableMap allJavaResources =
IncrementalRelativeFileSets.fromZipsAndDirectories(getJavaResourceFiles());
ImmutableMap allAssets =
IncrementalRelativeFileSets.fromZipsAndDirectories(
Collections.singleton(getAssets()));
ImmutableMap allAndroidResources =
IncrementalRelativeFileSets.fromZipsAndDirectories(androidResources);
ImmutableMap allJniResources =
IncrementalRelativeFileSets.fromZipsAndDirectories(getJniFolders());
ImmutableMap allAtomMetadataFiles;
if (getAtomMetadataFolder() == null) {
allAtomMetadataFiles = ImmutableMap.of();
} else {
allAtomMetadataFiles =
IncrementalRelativeFileSets.fromDirectory(getAtomMetadataFolder());
}
saveData.setInputSet(allDex.keySet(), InputSet.DEX);
saveData.setInputSet(allJavaResources.keySet(), InputSet.JAVA_RESOURCE);
saveData.setInputSet(allAssets.keySet(), InputSet.ASSET);
saveData.setInputSet(allAndroidResources.keySet(), InputSet.ANDROID_RESOURCE);
saveData.setInputSet(allJniResources.keySet(), InputSet.NATIVE_RESOURCE);
saveData.setInputSet(allAtomMetadataFiles.keySet(), InputSet.ATOM_METADATA);
saveData.saveCurrentData();
}
/**
* Obtains all changed inputs of a given input set. Given a set of files mapped to their
* changed status, this method returns a list of changes computed as follows:
*
*
* - Changed inputs are split into deleted and non-deleted inputs. This separation is
* needed because deleted inputs may no longer be mappable to any {@link InputSet} just
* by looking at the file path, without using {@link KnownFilesSaveData}.
*
- Deleted inputs are filtered through {@link KnownFilesSaveData} to get only those
* whose input set matches {@code inputSet}.
*
- Non-deleted inputs are processed through
* {@link IncrementalRelativeFileSets#makeFromBaseFiles(Collection, Map, FileCacheByPath,
* Set)}
* to obtain the incremental file changes.
*
- The results of processed deleted and non-deleted are merged and returned.
*
*
* @param changedInputs all changed inputs
* @param saveData the save data with all input sets from last run
* @param inputSet the input set to filter
* @param baseFiles the base files of the input set
* @param cacheByPath where to cache files
* @param cacheUpdates receives the runnables that will update the cache
* @return the status of all relative files in the input set
*/
@NonNull
private ImmutableMap getChangedInputs(
@NonNull Map changedInputs,
@NonNull KnownFilesSaveData saveData,
@NonNull InputSet inputSet,
@NonNull Collection baseFiles,
@NonNull FileCacheByPath cacheByPath,
@NonNull Set cacheUpdates)
throws IOException {
/*
* Figure out changes to deleted files.
*/
Set deletedFiles =
Maps.filterValues(changedInputs, Predicates.equalTo(FileStatus.REMOVED)).keySet();
Set deletedRelativeFiles = saveData.find(deletedFiles, inputSet);
/*
* Figure out changes to non-deleted files.
*/
Map nonDeletedFiles =
Maps.filterValues(
changedInputs,
Predicates.not(Predicates.equalTo(FileStatus.REMOVED)));
Map nonDeletedRelativeFiles =
IncrementalRelativeFileSets.makeFromBaseFiles(
baseFiles,
nonDeletedFiles,
cacheByPath,
cacheUpdates);
/*
* Merge everything.
*/
return new ImmutableMap.Builder()
.putAll(Maps.asMap(deletedRelativeFiles, Functions.constant(FileStatus.REMOVED)))
.putAll(nonDeletedRelativeFiles)
.build();
}
/**
* Creates the new instant run resources from the dex files. This method is not
* incremental. It will ignore updates and look at all dex files and always rebuild the
* instant run resources.
*
* The instant run resources are resources that package dex files.
*
* @param dexBaseFiles the base files to dex
* @return the instant run resources
* @throws IOException failed to create the instant run resources
*/
@NonNull
private ImmutableMap makeInstantRunResourcesFromDex(
@NonNull Iterable dexBaseFiles) throws IOException {
File tmpZipFile = new File(instantRunSupportDir, "instant-run.zip");
boolean existedBefore = tmpZipFile.exists();
Files.createParentDirs(tmpZipFile);
ZipOutputStream zipFile = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(tmpZipFile)));
// no need to compress a zip, the APK itself gets compressed.
zipFile.setLevel(0);
try {
for (File dexFolder : dexBaseFiles) {
for (File file : Files.fileTreeTraverser().breadthFirstTraversal(dexFolder)) {
if (file.isFile() && file.getName().endsWith(SdkConstants.DOT_DEX)) {
// There are several pieces of code in the runtime library that depend
// on this exact pattern, so it should not be changed without thorough
// testing (it's basically part of the contract).
String entryName =
file.getParentFile().getName() + "-" + file.getName();
zipFile.putNextEntry(new ZipEntry(entryName));
try {
Files.copy(file, zipFile);
} finally {
zipFile.closeEntry();
}
}
}
}
} finally {
zipFile.close();
}
RelativeFile resourcesFile = new RelativeFile(instantRunSupportDir, tmpZipFile);
return ImmutableMap.of(resourcesFile, existedBefore? FileStatus.CHANGED : FileStatus.NEW);
}
// ----- FileSupplierTask -----
@Override
public File get() {
return getOutputFile();
}
@NonNull
@Override
public Task getTask() {
return this;
}
/**
* Class that keeps track of which files are known in incremental builds. Gradle tells us
* which files were modified, but doesn't tell us which inputs the files come from so when a
* file is marked as deleted, we don't know which input set it was deleted from. This class
* maintains the list of files and their source locations and can be saved to the intermediate
* directory.
*
* File data is loaded on creation and saved on close.
*
*
Implementation note: the actual data is saved in a property file with the
* file name mapped to the name of the {@link InputSet} enum defining its input set.
*/
private static class KnownFilesSaveData {
/**
* Name of the file with the save data.
*/
private static final String SAVE_DATA_FILE_NAME = "file-input-save-data.txt";
/**
* Property with the number of files in the property file.
*/
private static final String COUNT_PROPERTY = "count";
/**
* Suffix for property with the base file.
*/
private static final String BASE_SUFFIX = ".base";
/**
* Suffix for property with the file.
*/
private static final String FILE_SUFFIX = ".file";
/**
* Suffix for property with the input set.
*/
private static final String INPUT_SET_SUFFIX = ".set";
/**
* Cache with all known cached files.
*/
private static final Map> mCache =
Maps.newHashMap();
/**
* File contents cache.
*/
@NonNull
private final CachedFileContents mFileContentsCache;
/**
* Maps all files in the last build to their input set.
*/
@NonNull
private final Map mFiles;
/**
* Has the data been modified?
*/
private boolean mDirty;
/**
* Creates a new file save data and reads it one exists. To create new instances, the
* factory method {@link #make(File)} should be used.
*
* @param cache the cache used
* @throws IOException failed to read the file (not thrown if the file does not exist)
*/
private KnownFilesSaveData(@NonNull CachedFileContents cache)
throws IOException {
mFileContentsCache = cache;
mFiles = Maps.newHashMap();
if (cache.getFile().isFile()) {
readCurrentData();
}
mDirty = false;
}
/**
* Creates a new {@link KnownFilesSaveData}, or obtains one from cache if there already
* exists a cached entry.
*
* @param intermediateDir the intermediate directory where the cache is stored
* @return the save data
* @throws IOException save data file exists but there was an error reading it (not thrown
* if the file does not exist)
*/
@NonNull
private static synchronized KnownFilesSaveData make(@NonNull File intermediateDir)
throws IOException {
File saveFile = computeSaveFile(intermediateDir);
CachedFileContents cached = mCache.get(saveFile);
if (cached == null) {
cached = new CachedFileContents<>(saveFile);
mCache.put(saveFile, cached);
}
KnownFilesSaveData saveData = cached.getCache();
if (saveData == null) {
saveData = new KnownFilesSaveData(cached);
cached.closed(saveData);
}
return saveData;
}
/**
* Computes what is the save file for the provided intermediate directory.
*
* @param intermediateDir the intermediate directory
* @return the file
*/
private static File computeSaveFile(@NonNull File intermediateDir) {
return new File(intermediateDir, SAVE_DATA_FILE_NAME);
}
/**
* Reads the save file data into the in-memory data structures.
*
* @throws IOException failed to read the file
*/
private void readCurrentData() throws IOException {
Closer closer = Closer.create();
File saveFile = mFileContentsCache.getFile();
Properties properties = new Properties();
try {
Reader saveDataReader = closer.register(new FileReader(saveFile));
properties.load(saveDataReader);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
String fileCountText = null;
int fileCount;
try {
fileCountText = properties.getProperty(COUNT_PROPERTY);
if (fileCountText == null) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + COUNT_PROPERTY + "' has no value).");
}
fileCount = Integer.parseInt(fileCountText);
if (fileCount < 0) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + COUNT_PROPERTY + "' has value " + fileCount + ").");
}
} catch (NumberFormatException e) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + COUNT_PROPERTY + "' has value '" + fileCountText + "').",
e);
}
for (int i = 0; i < fileCount; i++) {
String baseName = properties.getProperty(i + BASE_SUFFIX);
if (baseName == null) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + i + BASE_SUFFIX + "' has no value).");
}
String fileName = properties.getProperty(i + FILE_SUFFIX);
if (fileName == null) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + i + FILE_SUFFIX + "' has no value).");
}
String inputSetName = properties.getProperty(i + INPUT_SET_SUFFIX);
if (inputSetName == null) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + i + INPUT_SET_SUFFIX + "' has no value).");
}
InputSet is;
try {
is = InputSet.valueOf(InputSet.class, inputSetName);
} catch (IllegalArgumentException e) {
throw new IOException("Invalid data stored in file '" + saveFile + "' ("
+ "property '" + i + INPUT_SET_SUFFIX + "' has invalid value '"
+ inputSetName + "').");
}
mFiles.put(new RelativeFile(new File(baseName), new File(fileName)), is);
}
}
/**
* Saves current in-memory data structures to file.
*
* @throws IOException failed to save the data
*/
private void saveCurrentData() throws IOException {
if (!mDirty) {
return;
}
Closer closer = Closer.create();
Properties properties = new Properties();
properties.put(COUNT_PROPERTY, Integer.toString(mFiles.size()));
int idx = 0;
for (Map.Entry e : mFiles.entrySet()) {
RelativeFile rf = e.getKey();
String basePath = Verify.verifyNotNull(rf.getBase().getPath());
Verify.verify(!basePath.isEmpty());
String filePath = Verify.verifyNotNull(rf.getFile().getPath());
Verify.verify(!filePath.isEmpty());
properties.put(idx + BASE_SUFFIX, basePath);
properties.put(idx + FILE_SUFFIX, filePath);
properties.put(idx + INPUT_SET_SUFFIX, e.getValue().name());
idx++;
}
try {
Writer saveDataWriter = closer.register(new FileWriter(
mFileContentsCache.getFile()));
properties.store(saveDataWriter, "Internal package file, do not edit.");
mFileContentsCache.closed(this);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
/**
* Obtains all relative files stored in the save data that have the provided input set and
* whose files are included in the provided set of files. This method allows retrieving
* the original relative files from the files, while filtering for the desired input set.
*
* @param files the files to filter
* @param inputSet the input set to filter
* @return all saved relative files that have the given input set and whose files exist
* in the provided set
*/
@NonNull
private ImmutableSet find(@NonNull Set files,
@NonNull InputSet inputSet) {
Set found = Sets.newHashSet();
for (RelativeFile rf :
Maps.filterValues(mFiles, Predicates.equalTo(inputSet)).keySet()) {
if (files.contains(rf.getFile())) {
found.add(rf);
}
}
return ImmutableSet.copyOf(found);
}
/**
* Obtains a predicate that checks if a file is in an input set.
*
* @param inputSet the input set
* @return the predicate
*/
@NonNull
private Function inInputSet(@NonNull InputSet inputSet) {
Map inverseFiltered = mFiles.entrySet().stream()
.filter(e -> e.getValue() == inputSet)
.map(Map.Entry::getKey)
.collect(
HashMap::new,
(m, rf) -> m.put(rf.getFile(), rf),
Map::putAll);
return inverseFiltered::get;
}
/**
* Sets all files in an input set, replacing whatever existed previously.
*
* @param files the files
* @param set the input set
*/
private void setInputSet(@NonNull Collection files, @NonNull InputSet set) {
for (Iterator> it = mFiles.entrySet().iterator();
it.hasNext(); ) {
Map.Entry next = it.next();
if (next.getValue() == set && !files.contains(next.getKey())) {
it.remove();
mDirty = true;
}
}
files.forEach(f -> {
if (!mFiles.containsKey(f)) {
mFiles.put(f, set);
mDirty = true;
}
});
}
}
/**
* Input sets for files for save data (see {@link KnownFilesSaveData}).
*/
private enum InputSet {
/**
* File belongs to the dex file set.
*/
DEX,
/**
* File belongs to the java resources file set.
*/
JAVA_RESOURCE,
/**
* File belongs to the native resources file set.
*/
NATIVE_RESOURCE,
/**
* File belongs to the android resources file set.
*/
ANDROID_RESOURCE,
/**
* File belongs to the assets file set.
*/
ASSET,
/**
* File is the atom metadata.
*/
ATOM_METADATA
}
// ----- ConfigAction -----
public abstract static class ConfigAction
implements TaskConfigAction {
protected final PackagingScope packagingScope;
protected final DexPackagingPolicy dexPackagingPolicy;
public ConfigAction(
@NonNull PackagingScope packagingScope,
@Nullable InstantRunPatchingPolicy patchingPolicy) {
this.packagingScope = checkNotNull(packagingScope);
dexPackagingPolicy = patchingPolicy == null
? DexPackagingPolicy.STANDARD
: patchingPolicy.getDexPatchingPolicy();
}
@Override
public void execute(@NonNull final T packageAndroidArtifact) {
packageAndroidArtifact.instantRunFileType = FileType.MAIN;
packageAndroidArtifact.setAndroidBuilder(packagingScope.getAndroidBuilder());
packageAndroidArtifact.setVariantName(packagingScope.getFullVariantName());
packageAndroidArtifact.setMinSdkVersion(packagingScope.getMinSdkVersion());
packageAndroidArtifact.instantRunContext =
packagingScope.getInstantRunBuildContext();
packageAndroidArtifact.dexPackagingPolicy = dexPackagingPolicy;
packageAndroidArtifact.instantRunSupportDir =
packagingScope.getInstantRunSupportDir();
packageAndroidArtifact.setIncrementalFolder(
packagingScope.getIncrementalDir(packageAndroidArtifact.getName()));
packageAndroidArtifact.aaptOptions = packagingScope.getAaptOptions();
packageAndroidArtifact.manifest = packagingScope.getManifestFile();
File cacheByPathDir = new File(packageAndroidArtifact.getIncrementalFolder(),
ZIP_DIFF_CACHE_DIR);
FileUtils.mkdirs(cacheByPathDir);
packageAndroidArtifact.cacheByPath = new FileCacheByPath(cacheByPathDir);
ConventionMappingHelper.map(
packageAndroidArtifact, "resourceFile", packagingScope::getFinalResourcesFile);
ConventionMappingHelper.map(
packageAndroidArtifact, "dexFolders",
() -> packagingScope.getDexFolders());
ConventionMappingHelper.map(
packageAndroidArtifact,
"javaResourceFiles",
packagingScope::getJavaResources);
packageAndroidArtifact.setAssets(packagingScope.getAssetsDir());
ConventionMappingHelper.map(packageAndroidArtifact, "jniFolders", () -> {
if (packagingScope.getSplitHandlingPolicy() ==
SplitHandlingPolicy.PRE_21_POLICY) {
return packagingScope.getJniFolders();
}
Set filters =
AbiSplitOptions.getAbiFilters(packagingScope.getAbiFilters());
return filters.isEmpty()
? packagingScope.getJniFolders()
: Collections.emptySet();
});
ConventionMappingHelper.map(packageAndroidArtifact, "abiFilters", () -> {
String filter = packagingScope.getMainOutputFile().getFilter(
com.android.build.OutputFile.ABI);
if (filter != null) {
return ImmutableSet.of(filter);
}
Set supportedAbis = packagingScope.getSupportedAbis();
// TODO: nullability
if (supportedAbis != null) {
return supportedAbis;
}
return ImmutableSet.of();
});
ConventionMappingHelper.map(
packageAndroidArtifact, "jniDebugBuild", packagingScope::isJniDebuggable);
ConventionMappingHelper.map(
packageAndroidArtifact, "debugBuild", packagingScope::isDebuggable);
packageAndroidArtifact.setSigningConfig(packagingScope.getSigningConfig());
ConventionMappingHelper.map(
packageAndroidArtifact, "packagingOptions", packagingScope::getPackagingOptions);
}
}
}