All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.build.gradle.tasks.PackageAndroidArtifact Maven / Gradle / Ivy

The newest version!
/*
 * 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 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:
     *
     * 
    *
  1. 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}. *
  2. Deleted inputs are filtered through {@link KnownFilesSaveData} to get only those * whose input set matches {@code inputSet}. *
  3. Non-deleted inputs are processed through * {@link IncrementalRelativeFileSets#makeFromBaseFiles(Collection, Map, FileCacheByPath, * Set)} * to obtain the incremental file changes. *
  4. 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy