blob: 5bc6a6cc8ee7b2c0643f8024ac2d2e055b4ca92f [file] [log] [blame]
/*
* 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.EstimateLinearAllocStep;
import com.facebook.buck.java.JavaLibrary;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.rules.AbiRule;
import com.facebook.buck.rules.AbstractBuildRule;
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.ImmutableSha1HashCode;
import com.facebook.buck.rules.InitializableFromDisk;
import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.RmStep;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import java.nio.file.Path;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
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 AbiRule, HasBuildTarget, InitializableFromDisk<BuildOutput> {
@VisibleForTesting
static final String LINEAR_ALLOC_KEY_ON_DISK_METADATA = "linearalloc";
private final JavaLibrary javaLibrary;
private final BuildOutputInitializer<BuildOutput> buildOutputInitializer;
@VisibleForTesting
DexProducedFromJavaLibrary(
BuildRuleParams params,
SourcePathResolver resolver,
JavaLibrary javaLibrary) {
super(params, resolver);
this.javaLibrary = javaLibrary;
this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this);
}
@Override
public ImmutableCollection<Path> getInputsToCompareToOutput() {
// The deps of this rule already capture all of the inputs that should affect the cache key.
return ImmutableList.of();
}
@Override
public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
return builder;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context,
final BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
steps.add(new RmStep(getPathToDex(), /* shouldForceDeletion */ true));
// Make sure that the buck-out/gen/ directory exists for this.buildTarget.
steps.add(new MkdirStep(getPathToDex().getParent()));
// If there are classes, run dx.
final boolean hasClassesToDx = !javaLibrary.getClassNamesToHashes().isEmpty();
final Supplier<Integer> linearAllocEstimate;
if (hasClassesToDx) {
Path pathToOutputFile = javaLibrary.getPathToOutputFile();
EstimateLinearAllocStep estimate = new EstimateLinearAllocStep(pathToOutputFile);
steps.add(estimate);
linearAllocEstimate = 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.
DxStep dx = new DxStep(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);
} else {
linearAllocEstimate = 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 int execute(ExecutionContext context) {
if (hasClassesToDx) {
buildableContext.recordArtifact(getPathToDex());
}
buildableContext.addMetadata(LINEAR_ALLOC_KEY_ON_DISK_METADATA,
String.valueOf(linearAllocEstimate.get()));
return 0;
}
};
steps.add(recordArtifactAndMetadataStep);
return steps.build();
}
@Override
public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
int linearAllocEstimate = Integer.parseInt(
onDiskBuildInfo.getValue(LINEAR_ALLOC_KEY_ON_DISK_METADATA).get());
return new BuildOutput(linearAllocEstimate);
}
@Override
public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() {
return buildOutputInitializer;
}
static class BuildOutput {
private final int linearAllocEstimate;
private BuildOutput(int linearAllocEstimate) {
this.linearAllocEstimate = linearAllocEstimate;
}
}
@Override
@Nullable
public Path getPathToOutputFile() {
// A .dex file is not guaranteed to be generated, so we return null to be conservative.
return null;
}
public Path getPathToDex() {
return BuildTargets.getGenPath(getBuildTarget(), "%s.dex.jar");
}
public boolean hasOutput() {
return !getClassNames().isEmpty();
}
ImmutableSortedMap<String, HashCode> getClassNames() {
// TODO(mbolin): Assert that this Buildable has been built. Currently, there is no way to do
// that from a Buildable (but there is from an AbstractCachingBuildRule).
return javaLibrary.getClassNamesToHashes();
}
int getLinearAllocEstimate() {
return buildOutputInitializer.getBuildOutput().linearAllocEstimate;
}
/**
* The only dep for this rule should be {@link #javaLibrary}. Therefore, the ABI key for the deps
* of this buildable is the hash of the {@code .class} files for {@link #javaLibrary}.
*/
@Override
public Sha1HashCode getAbiKeyForDeps() {
return computeAbiKey(javaLibrary.getClassNamesToHashes());
}
@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 ImmutableSha1HashCode.of(hasher.hash().toString());
}
}