/* * Copyright 2013-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.DexProducedFromJavaLibrary.BuildOutput; import com.facebook.buck.dalvik.EstimateDexWeightStep; import com.facebook.buck.jvm.java.JavaLibrary; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.AddToRuleKey; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildOutputInitializer; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.InitializableFromDisk; import com.facebook.buck.rules.OnDiskBuildInfo; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey; import com.facebook.buck.step.AbstractExecutionStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.step.fs.MkdirStep; import com.facebook.buck.step.fs.RmStep; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.sha1.Sha1HashCode; import com.facebook.buck.zip.ZipScrubberStep; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.hash.HashCode; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import java.io.IOException; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; /** * {@link DexProducedFromJavaLibrary} is a {@link BuildRule} that serves a very specific purpose: it * takes a {@link JavaLibrary} and dexes the output of the {@link JavaLibrary} if its list of * classes is non-empty. Because it is expected to be used with pre-dexing, we always pass the * {@code --force-jumbo} flag to {@code dx} in this buildable. * * <p>Most {@link BuildRule}s can determine the (possibly null) path to their output file from their * definition. This is an anomaly because we do not know whether this will write a {@code .dex} file * until runtime. Unfortunately, because there is no such thing as an empty {@code .dex} file, we * cannot write a meaningful "dummy .dex" if there are no class files to pass to {@code dx}. */ public class DexProducedFromJavaLibrary extends AbstractBuildRule implements SupportsInputBasedRuleKey, InitializableFromDisk<BuildOutput> { @VisibleForTesting static final String WEIGHT_ESTIMATE = "weight_estimate"; @VisibleForTesting static final String CLASSNAMES_TO_HASHES = "classnames_to_hashes"; @VisibleForTesting static final String REFERENCED_RESOURCES = "referenced_resources"; @AddToRuleKey private final SourcePath javaLibrarySourcePath; private final JavaLibrary javaLibrary; private final BuildOutputInitializer<BuildOutput> buildOutputInitializer; DexProducedFromJavaLibrary(BuildRuleParams params, JavaLibrary javaLibrary) { super(params); this.javaLibrary = javaLibrary; this.javaLibrarySourcePath = javaLibrary.getSourcePathToOutput(); this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); steps.add(RmStep.of(getProjectFilesystem(), getPathToDex())); // Make sure that the buck-out/gen/ directory exists for this.buildTarget. steps.add(MkdirStep.of(getProjectFilesystem(), getPathToDex().getParent())); // If there are classes, run dx. final ImmutableSortedMap<String, HashCode> classNamesToHashes = javaLibrary.getClassNamesToHashes(); final boolean hasClassesToDx = !classNamesToHashes.isEmpty(); final Supplier<Integer> weightEstimate; @Nullable final DxStep dx; if (hasClassesToDx) { Path pathToOutputFile = context.getSourcePathResolver().getAbsolutePath(javaLibrarySourcePath); EstimateDexWeightStep estimate = new EstimateDexWeightStep(getProjectFilesystem(), pathToOutputFile); steps.add(estimate); weightEstimate = estimate; // To be conservative, use --force-jumbo for these intermediate .dex files so that they can be // merged into a final classes.dex that uses jumbo instructions. dx = new DxStep( getProjectFilesystem(), getPathToDex(), Collections.singleton(pathToOutputFile), EnumSet.of( DxStep.Option.USE_CUSTOM_DX_IF_AVAILABLE, DxStep.Option.RUN_IN_PROCESS, DxStep.Option.NO_OPTIMIZE, DxStep.Option.FORCE_JUMBO)); steps.add(dx); // The `DxStep` delegates to android tools to build a ZIP with timestamps in it, making // the output non-deterministic. So use an additional scrubbing step to zero these out. steps.add(ZipScrubberStep.of(getProjectFilesystem().resolve(getPathToDex()))); } else { dx = null; weightEstimate = Suppliers.ofInstance(0); } // Run a step to record artifacts and metadata. The values recorded depend upon whether dx was // run. String stepName = hasClassesToDx ? "record_dx_success" : "record_empty_dx"; AbstractExecutionStep recordArtifactAndMetadataStep = new AbstractExecutionStep(stepName) { @Override public StepExecutionResult execute(ExecutionContext context) throws IOException { if (hasClassesToDx) { buildableContext.recordArtifact(getPathToDex()); @Nullable Collection<String> referencedResources = dx.getResourcesReferencedInCode(); if (referencedResources != null) { buildableContext.addMetadata( REFERENCED_RESOURCES, Ordering.natural().immutableSortedCopy(referencedResources)); } } buildableContext.addMetadata(WEIGHT_ESTIMATE, String.valueOf(weightEstimate.get())); // Record the classnames to hashes map. buildableContext.addMetadata( CLASSNAMES_TO_HASHES, ObjectMappers.WRITER.writeValueAsString( Maps.transformValues(classNamesToHashes, Object::toString))); return StepExecutionResult.SUCCESS; } }; steps.add(recordArtifactAndMetadataStep); return steps.build(); } @Override public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) throws IOException { int weightEstimate = Integer.parseInt(onDiskBuildInfo.getValue(WEIGHT_ESTIMATE).get()); Map<String, String> map = ObjectMappers.readValue( onDiskBuildInfo.getValue(CLASSNAMES_TO_HASHES).get(), new TypeReference<Map<String, String>>() {}); Map<String, HashCode> classnamesToHashes = Maps.transformValues(map, HashCode::fromString); Optional<ImmutableList<String>> referencedResources = onDiskBuildInfo.getValues(REFERENCED_RESOURCES); return new BuildOutput( weightEstimate, ImmutableSortedMap.copyOf(classnamesToHashes), referencedResources); } @Override public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() { return buildOutputInitializer; } static class BuildOutput { private final int weightEstimate; private final ImmutableSortedMap<String, HashCode> classnamesToHashes; private final Optional<ImmutableList<String>> referencedResources; BuildOutput( int weightEstimate, ImmutableSortedMap<String, HashCode> classnamesToHashes, Optional<ImmutableList<String>> referencedResources) { this.weightEstimate = weightEstimate; this.classnamesToHashes = classnamesToHashes; this.referencedResources = referencedResources; } } @Override @Nullable public SourcePath getSourcePathToOutput() { // A .dex file is not guaranteed to be generated, so we return null to be conservative. return null; } public Path getPathToDex() { return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s.dex.jar"); } public boolean hasOutput() { return !getClassNames().isEmpty(); } ImmutableSortedMap<String, HashCode> getClassNames() { return buildOutputInitializer.getBuildOutput().classnamesToHashes; } int getWeightEstimate() { return buildOutputInitializer.getBuildOutput().weightEstimate; } Optional<ImmutableList<String>> getReferencedResources() { return buildOutputInitializer.getBuildOutput().referencedResources; } @VisibleForTesting static Sha1HashCode computeAbiKey(ImmutableSortedMap<String, HashCode> classNames) { Hasher hasher = Hashing.sha1().newHasher(); for (Map.Entry<String, HashCode> entry : classNames.entrySet()) { hasher.putUnencodedChars(entry.getKey()); hasher.putByte((byte) 0); hasher.putUnencodedChars(entry.getValue().toString()); hasher.putByte((byte) 0); } return Sha1HashCode.fromHashCode(hasher.hash()); } }