com.facebook.buck.android.PreDexMerge.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.android.PreDexMerge.java

Source

/*
 * Copyright 2014-present Facebook, Inc.
 *
 * 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.facebook.buck.android;

import com.facebook.buck.android.PreDexMerge.BuildOutput;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildable;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.InitializableFromDisk;
import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.rules.RecordFileSha1Step;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.rules.SourcePaths;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.util.MorePaths;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Suppliers;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Buildable that is responsible for:
 * <ul>
 *   <li>Bucketing pre-dexed jars into lists for primary and secondary dex files
 *       (if the app is split-dex).
 *   <li>Merging the pre-dexed jars into primary and secondary dex files.
 *   <li>Writing the split-dex "metadata.txt".
 * </ul>
 * <p>
 * Clients of this Buildable may need to know:
 * <ul>
 *   <li>The locations of the zip files directories containing secondary dex files and metadata.
 * </ul>
 *
 * This uses a separate implementation from addDexingSteps.
 * The differences in the splitting logic are too significant to make it
 * worth merging them.
 */
public class PreDexMerge extends AbstractBuildable implements InitializableFromDisk<BuildOutput> {

    private static final String PRIMARY_DEX_HASH_KEY = "primary_dex_hash";
    private static final String SECONDARY_DEX_DIRECTORIES_KEY = "secondary_dex_directories";

    private final BuildTarget buildTarget;
    private final Path primaryDexPath;
    private final DexSplitMode dexSplitMode;
    private final ImmutableSet<DexProducedFromJavaLibrary> preDexDeps;
    private final UberRDotJava uberRDotJava;
    private final BuildOutputInitializer<BuildOutput> buildOutputInitializer;

    public PreDexMerge(BuildTarget buildTarget, Path primaryDexPath, DexSplitMode dexSplitMode,
            ImmutableSet<DexProducedFromJavaLibrary> preDexDeps, UberRDotJava uberRDotJava) {
        this.buildTarget = Preconditions.checkNotNull(buildTarget);
        this.primaryDexPath = Preconditions.checkNotNull(primaryDexPath);
        this.dexSplitMode = Preconditions.checkNotNull(dexSplitMode);
        this.preDexDeps = Preconditions.checkNotNull(preDexDeps);
        this.uberRDotJava = Preconditions.checkNotNull(uberRDotJava);
        this.buildOutputInitializer = new BuildOutputInitializer<>(buildTarget, this);
    }

    @Override
    public Collection<Path> getInputsToCompareToOutput() {
        return SourcePaths.filterInputsToCompareToOutput(dexSplitMode.getSourcePaths());
    }

    @Override
    public List<Step> getBuildSteps(BuildContext context, BuildableContext buildableContext) {
        ImmutableList.Builder<Step> steps = ImmutableList.builder();
        steps.add(new MkdirStep(primaryDexPath.getParent()));

        if (dexSplitMode.isShouldSplitDex()) {
            addStepsForSplitDex(steps, context, buildableContext);
        } else {
            addStepsForSingleDex(steps, buildableContext);
        }
        return steps.build();
    }

    /**
     * Wrapper class for all the paths we need when merging for a split-dex APK.
     */
    private final class SplitDexPaths {
        private final Path metadataDir;
        private final Path jarfilesDir;
        private final Path scratchDir;
        private final Path successDir;
        private final Path metadataSubdir;
        private final Path jarfilesSubdir;
        private final Path metadataFile;

        private SplitDexPaths() {
            Path workDir = BuildTargets.getBinPath(buildTarget, "_%s_output");

            metadataDir = workDir.resolve("metadata");
            jarfilesDir = workDir.resolve("jarfiles");
            scratchDir = workDir.resolve("scratch");
            successDir = workDir.resolve("success");
            // These directories must use SECONDARY_DEX_SUBDIR because that mirrors the paths that
            // they will appear at in the APK.
            metadataSubdir = metadataDir.resolve(AndroidBinary.SECONDARY_DEX_SUBDIR);
            jarfilesSubdir = jarfilesDir.resolve(AndroidBinary.SECONDARY_DEX_SUBDIR);
            metadataFile = metadataSubdir.resolve("metadata.txt");
        }
    }

    private void addStepsForSplitDex(ImmutableList.Builder<Step> steps, BuildContext context,
            BuildableContext buildableContext) {

        // Collect all of the DexWithClasses objects to use for merging.
        ImmutableList<DexWithClasses> dexFilesToMerge = FluentIterable.from(preDexDeps)
                .transform(DexWithClasses.TO_DEX_WITH_CLASSES).filter(Predicates.notNull()).toList();

        final SplitDexPaths paths = new SplitDexPaths();

        final ImmutableSet<Path> secondaryDexDirectories = ImmutableSet.of(paths.metadataDir, paths.jarfilesDir);

        // Do not clear existing directory which might contain secondary dex files that are not
        // re-merged (since their contents did not change).
        steps.add(new MkdirStep(paths.jarfilesSubdir));
        steps.add(new MkdirStep(paths.successDir));

        steps.add(new MakeCleanDirectoryStep(paths.metadataSubdir));
        steps.add(new MakeCleanDirectoryStep(paths.scratchDir));

        buildableContext.addMetadata(SECONDARY_DEX_DIRECTORIES_KEY,
                Iterables.transform(secondaryDexDirectories, Functions.toStringFunction()));

        buildableContext.recordArtifact(primaryDexPath);
        buildableContext.recordArtifactsInDirectory(paths.jarfilesSubdir);
        buildableContext.recordArtifactsInDirectory(paths.metadataSubdir);
        buildableContext.recordArtifactsInDirectory(paths.successDir);

        PreDexedFilesSorter preDexedFilesSorter = new PreDexedFilesSorter(uberRDotJava.getRDotJavaDexWithClasses(),
                dexFilesToMerge, dexSplitMode.getPrimaryDexPatterns(), paths.scratchDir,
                dexSplitMode.getLinearAllocHardLimit(), dexSplitMode.getDexStore(), paths.jarfilesSubdir);
        final PreDexedFilesSorter.Result sortResult = preDexedFilesSorter.sortIntoPrimaryAndSecondaryDexes(context,
                steps);

        steps.add(new SmartDexingStep(primaryDexPath, Suppliers.ofInstance(sortResult.primaryDexInputs),
                Optional.of(paths.jarfilesSubdir),
                Optional.of(Suppliers.ofInstance(sortResult.secondaryOutputToInputs)),
                sortResult.dexInputHashesProvider, paths.successDir, /* numThreads */ Optional.<Integer>absent(),
                AndroidBinary.DX_MERGE_OPTIONS));

        // Record the primary dex SHA1 so exopackage apks can use it to compute their ABI keys.
        // Single dex apks cannot be exopackages, so they will never need ABI keys.
        steps.add(new RecordFileSha1Step(primaryDexPath, PRIMARY_DEX_HASH_KEY, buildableContext));

        steps.add(new AbstractExecutionStep("write_metadata_txt") {
            @Override
            public int execute(ExecutionContext executionContext) {
                ProjectFilesystem filesystem = executionContext.getProjectFilesystem();
                Map<Path, DexWithClasses> metadataTxtEntries = sortResult.metadataTxtDexEntries;
                List<String> lines = Lists.newArrayListWithCapacity(metadataTxtEntries.size());
                try {
                    for (Map.Entry<Path, DexWithClasses> entry : metadataTxtEntries.entrySet()) {
                        Path pathToSecondaryDex = entry.getKey();
                        String containedClass = Iterables.get(entry.getValue().getClassNames(), 0);
                        containedClass = containedClass.replace('/', '.');
                        String hash = filesystem.computeSha1(pathToSecondaryDex);
                        lines.add(
                                String.format("%s %s %s", pathToSecondaryDex.getFileName(), hash, containedClass));
                    }
                    filesystem.writeLinesToPath(lines, paths.metadataFile);
                } catch (IOException e) {
                    executionContext.logError(e, "Failed when writing metadata.txt multi-dex.");
                    return 1;
                }
                return 0;
            }
        });
    }

    private void addStepsForSingleDex(ImmutableList.Builder<Step> steps, final BuildableContext buildableContext) {
        // For single-dex apps with pre-dexing, we just add the steps directly.
        Iterable<Path> filesToDex = FluentIterable.from(preDexDeps)
                .transform(new Function<DexProducedFromJavaLibrary, Path>() {
                    @Override
                    @Nullable
                    public Path apply(DexProducedFromJavaLibrary preDex) {
                        if (preDex.hasOutput()) {
                            return preDex.getPathToDex();
                        } else {
                            return null;
                        }
                    }
                }).filter(Predicates.notNull());

        // If this APK has Android resources, then the generated R.class files also need to be dexed.
        Optional<DexWithClasses> rDotJavaDexWithClasses = uberRDotJava.getRDotJavaDexWithClasses();
        if (rDotJavaDexWithClasses.isPresent()) {
            filesToDex = Iterables.concat(filesToDex,
                    Collections.singleton(rDotJavaDexWithClasses.get().getPathToDexFile()));
        }

        buildableContext.recordArtifact(primaryDexPath);

        // This will combine the pre-dexed files and the R.class files into a single classes.dex file.
        steps.add(new DxStep(primaryDexPath, filesToDex, AndroidBinary.DX_MERGE_OPTIONS));

        buildableContext.addMetadata(SECONDARY_DEX_DIRECTORIES_KEY, ImmutableSet.<String>of());
    }

    public Path getMetadataTxtPath() {
        Preconditions.checkState(dexSplitMode.isShouldSplitDex());
        return new SplitDexPaths().metadataFile;
    }

    public Path getDexDirectory() {
        Preconditions.checkState(dexSplitMode.isShouldSplitDex());
        return new SplitDexPaths().jarfilesSubdir;
    }

    @Override
    public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
        dexSplitMode.appendToRuleKey("dexSplitMode", builder);
        return builder;
    }

    @Nullable
    @Override
    public Path getPathToOutputFile() {
        return null;
    }

    public Sha1HashCode getPrimaryDexHash() {
        Preconditions.checkState(dexSplitMode.isShouldSplitDex());
        return buildOutputInitializer.getBuildOutput().primaryDexHash;
    }

    public ImmutableSet<Path> getSecondaryDexDirectories() {
        return buildOutputInitializer.getBuildOutput().secondaryDexDirectories;
    }

    static class BuildOutput {
        /** Null iff this is a single-dex app. */
        @Nullable
        private final Sha1HashCode primaryDexHash;
        private final ImmutableSet<Path> secondaryDexDirectories;

        BuildOutput(Sha1HashCode primaryDexHash, ImmutableSet<Path> secondaryDexDirectories) {
            this.primaryDexHash = primaryDexHash;
            this.secondaryDexDirectories = Preconditions.checkNotNull(secondaryDexDirectories);
        }
    }

    @Override
    public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
        Optional<Sha1HashCode> primaryDexHash = onDiskBuildInfo.getHash(PRIMARY_DEX_HASH_KEY);
        // We only save the hash for split-dex builds.
        if (dexSplitMode.isShouldSplitDex()) {
            Preconditions.checkState(primaryDexHash.isPresent());
        }

        return new BuildOutput(primaryDexHash.orNull(),
                FluentIterable.from(onDiskBuildInfo.getValues(SECONDARY_DEX_DIRECTORIES_KEY).get())
                        .transform(MorePaths.TO_PATH).toSet());
    }

    @Override
    public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() {
        return buildOutputInitializer;
    }
}