| /* |
| * Copyright 2012-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.rules; |
| |
| import com.facebook.buck.step.Step; |
| import com.facebook.buck.step.StepRunner; |
| import com.facebook.buck.util.BuckConstant; |
| import com.facebook.buck.util.DirectoryTraverser; |
| import com.facebook.buck.util.DirectoryTraversers; |
| import com.facebook.buck.util.MoreFiles; |
| import com.facebook.buck.util.ProjectFilesystem; |
| import com.facebook.buck.util.TriState; |
| import com.google.common.annotations.Beta; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.Files; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.SettableFuture; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.logging.Logger; |
| |
| import javax.annotation.Nullable; |
| |
| @Beta |
| public abstract class AbstractCachingBuildRule extends AbstractBuildRule implements BuildRule { |
| |
| private final static Logger logger = Logger.getLogger(AbstractCachingBuildRule.class.getName()); |
| |
| private final ArtifactCache artifactCache; |
| |
| /** |
| * This field will initially be UNSPECIFIED. Once it has been determined whether this rule is |
| * cached, this field will be set accordingly. Thus, the result of {@link #isCached(BuildContext)} |
| * itself is cached. |
| */ |
| private TriState isRuleCached; |
| |
| /** |
| * This field behaves similarly to isRuleCached, but instead tracks whether or not any of this |
| * rule's descendants are uncached. |
| */ |
| private TriState hasUncachedDescendants; |
| |
| /** |
| * This field behaves similarly to isRuleCached, but instead tracks whether or not any of this |
| * rule's inputs were uncached. |
| */ |
| private TriState ruleInputsAreCached; |
| |
| private ListenableFuture<BuildRuleSuccess> buildRuleResult; |
| |
| private Iterable<InputRule> inputsToCompareToOutputs; |
| |
| @Nullable private ImmutableSet<BuildRule> depsWithUncachedDescendantsCache = null; |
| |
| protected AbstractCachingBuildRule(CachingBuildRuleParams cachingBuildRuleParams) { |
| super(cachingBuildRuleParams); |
| this.artifactCache = cachingBuildRuleParams.getArtifactCache(); |
| this.isRuleCached = TriState.UNSPECIFIED; |
| this.hasUncachedDescendants = TriState.UNSPECIFIED; |
| this.ruleInputsAreCached = TriState.UNSPECIFIED; |
| } |
| |
| /** |
| * This rule is designed to be used for precondition checks in subclasses. For examples, before |
| * running the tests associated with a build rule, it is reasonable to do a sanity check to |
| * ensure that the rule has been built. |
| */ |
| protected final synchronized boolean isRuleBuilt() { |
| if (buildRuleResult == null) { |
| return false; |
| } else if (buildRuleResult.isDone()) { |
| // If the ListenableFuture<BuildRuleSuccess> is complete, the only way to verify that it |
| // completed successfully is to try invoking its get() method. |
| try { |
| buildRuleResult.get(); |
| return true; |
| } catch (ExecutionException e) { |
| return false; |
| } catch (InterruptedException e) { |
| return false; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| protected boolean isRuleBuiltFromCache() { |
| Preconditions.checkArgument(isRuleCached.isSet(), |
| "rule must be built before this method is invoked"); |
| return isRuleCached.asBoolean(); |
| } |
| |
| @Override |
| public final boolean isCached(BuildContext context) throws IOException { |
| if (isRuleCached.isSet()) { |
| return isRuleCached.asBoolean(); |
| } |
| |
| boolean isCached = checkIsCached(context, logger); |
| isRuleCached = TriState.forBooleanValue(isCached); |
| return isRuleCached.asBoolean(); |
| } |
| |
| private ImmutableSet<BuildRule> getDepsWithUncachedDescendants(final BuildContext context) |
| throws IOException { |
| if (depsWithUncachedDescendantsCache != null) { |
| return depsWithUncachedDescendantsCache; |
| } |
| |
| ImmutableSet.Builder<BuildRule> depsWithUncachedDescendantsBuilder = ImmutableSet.builder(); |
| for (BuildRule buildRule : getDeps()) { |
| if (buildRule.hasUncachedDescendants(context)) { |
| depsWithUncachedDescendantsBuilder.add(buildRule); |
| } |
| } |
| |
| depsWithUncachedDescendantsCache = depsWithUncachedDescendantsBuilder.build(); |
| |
| return depsWithUncachedDescendantsCache; |
| } |
| |
| @Override |
| public boolean hasUncachedDescendants(final BuildContext context) throws IOException { |
| if (hasUncachedDescendants.isSet()) { |
| return hasUncachedDescendants.asBoolean(); |
| } |
| |
| if (!isCached(context)) { |
| hasUncachedDescendants = TriState.TRUE; |
| } else { |
| hasUncachedDescendants = |
| TriState.forBooleanValue(!getDepsWithUncachedDescendants(context).isEmpty()); |
| } |
| return hasUncachedDescendants.asBoolean(); |
| } |
| |
| private Iterable<BuildRule> getRulesToConsiderForCaching() { |
| List<BuildRule> rules = Lists.newArrayList(); |
| // Not possible due to generics limitations: |
| // rules.addAll(getInputs()); |
| for (InputRule input : getInputs()) { |
| rules.add(input); |
| } |
| rules.add(this); |
| return rules; |
| } |
| |
| @VisibleForTesting |
| public void setIsCached(boolean isCached) { |
| isRuleCached = TriState.forBooleanValue(isCached); |
| } |
| |
| private Iterable<String> getSuccessFileStringsForBuildRules(Iterable<BuildRule> buildRules, |
| boolean mangleNonIdempotent) { |
| List<String> lines = Lists.newArrayList(); |
| for (BuildRule buildRule : buildRules) { |
| if (buildRule.getOutput() != null) { |
| lines.add(String.format("%s %s %s", |
| buildRule.getOutputKey().toString(mangleNonIdempotent), |
| buildRule.getRuleKey().toString(mangleNonIdempotent), |
| buildRule.getFullyQualifiedName())); |
| } |
| } |
| return lines; |
| } |
| |
| private boolean isMatchingSuccessState(BuildContext context, Iterable<BuildRule> buildRules, |
| String pathRelativeToProjectRoot) throws IOException { |
| ProjectFilesystem projectFilesystem = context.getProjectFilesystem(); |
| return projectFilesystem.isMatchingFileContents( |
| getSuccessFileStringsForBuildRules(buildRules, true), pathRelativeToProjectRoot); |
| } |
| |
| protected boolean ruleInputsCached(BuildContext context, Logger logger) throws IOException { |
| if (ruleInputsAreCached.isSet()) { |
| return ruleInputsAreCached.asBoolean(); |
| } |
| ruleInputsAreCached = TriState.forBooleanValue(checkRuleInputsCached(context, logger)); |
| |
| return ruleInputsAreCached.asBoolean(); |
| } |
| |
| private boolean checkRuleInputsCached(BuildContext context, Logger logger) throws IOException { |
| // If the success file does not exist, then this rule is not cached. |
| String pathToSuccessFile = getPathToSuccessFile(); |
| if (!context.getProjectFilesystem().exists(pathToSuccessFile)) { |
| logger.info(String.format("%s not cached because the output file %s is not built", |
| this, |
| pathToSuccessFile)); |
| return false; |
| } |
| |
| // If any of the input files, output files, or the build rule have been modified since the last |
| // build, then this rule should not be cached. |
| Iterable<BuildRule> rulesToConsiderForCaching = getRulesToConsiderForCaching(); |
| if (!isMatchingSuccessState(context, rulesToConsiderForCaching, pathToSuccessFile)) { |
| logger.info(String.format( |
| "%s not cached because the inputs and/or their contents have changed", this)); |
| return false; |
| } |
| return true; |
| } |
| |
| @VisibleForTesting |
| boolean checkIsCached(BuildContext context, Logger logger) throws IOException { |
| // First, check whether all of the deps are cached. |
| // This is checked first since it does not require touching the filesystem. |
| // If all of the deps were cached, check if the inputs to this rule were cached. |
| return depsCached(context, logger) && ruleInputsCached(context, logger); |
| } |
| |
| /** |
| * Checks to see if all of the dependencies rules are cached. By default, |
| * AbstractCachingBuildRule will consider a rule's deps uncached if any of its descendants were |
| * uncached. |
| */ |
| @VisibleForTesting |
| protected boolean depsCached(BuildContext context, Logger logger) throws IOException { |
| for (BuildRule dep : getDeps()) { |
| if (dep.hasUncachedDescendants(context)) { |
| logger.info(String.format("%s not cached because %s has an uncached descendant", |
| this, |
| dep.getFullyQualifiedName())); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Get the set of input files whose contents should be hashed for the purpose of determining |
| * whether this rule is cached. |
| * <p> |
| * Note that the collection of inputs is specified as a list, because for some build rules |
| * (such as {@link com.facebook.buck.shell.Genrule}), the order of the inputs is significant. If the order of the inputs |
| * is not significant for the build rule, then the list should be alphabetized so that lists with |
| * the same elements will be {@code .equals()} to one another. |
| */ |
| abstract protected Iterable<String> getInputsToCompareToOutput(BuildContext context); |
| |
| @Override |
| public final Iterable<InputRule> getInputs() { |
| if (inputsToCompareToOutputs == null) { |
| List<InputRule> inputs = Lists.newArrayList(); |
| for (String inputPath : getInputsToCompareToOutput(null /* context */)) { |
| inputs.add(new InputRule(inputPath)); |
| } |
| inputsToCompareToOutputs = inputs; |
| } |
| return inputsToCompareToOutputs; |
| } |
| |
| @Override |
| public final synchronized ListenableFuture<BuildRuleSuccess> build(final BuildContext context) { |
| // If buildRuleResult is non-null, then this method has already been invoked. Because this |
| // method must be idempotent, return the existing future. |
| if (buildRuleResult != null) { |
| // Not posting an event because the start has already been fired. |
| return buildRuleResult; |
| } |
| |
| context.getEventBus().post(BuildEvents.started(this)); |
| |
| // If this build rule is cached, then a result can be returned immediately. |
| boolean isCached; |
| ImmutableSet<BuildRule> depsWithUncachedDescendants; |
| try { |
| isCached = isCached(context); |
| depsWithUncachedDescendants = getDepsWithUncachedDescendants(context); |
| } catch (IOException e) { |
| // A failure while determining whether this build rule is cached should be treated as if this |
| // rule failed to build. |
| buildRuleResult = Futures.immediateFailedFuture(e); |
| // Assume a cache result is a miss: "Here's your cached result" should not be exceptional. |
| context.getEventBus().post( |
| BuildEvents.finished(this, BuildRuleStatus.FAIL, CacheResult.MISS)); |
| return buildRuleResult; |
| } |
| |
| buildRuleResult = SettableFuture.create(); |
| |
| // Create a single future to build all of the uncached descendants of this build rule. |
| ListenableFuture<List<BuildRuleSuccess>> builtDeps = Builder.getInstance().buildRules( |
| depsWithUncachedDescendants, context); |
| |
| if (isCached) { |
| logger.info(String.format("[FROM CACHE %s]", getFullyQualifiedName())); |
| context.getEventBus().post( |
| BuildEvents.finished(this, BuildRuleStatus.SUCCESS, CacheResult.HIT)); |
| |
| Futures.addCallback(builtDeps, new FutureCallback<List<BuildRuleSuccess>>() { |
| final SettableFuture<BuildRuleSuccess> result = |
| (SettableFuture<BuildRuleSuccess>)buildRuleResult; |
| |
| @Override |
| public void onSuccess(List<BuildRuleSuccess> results) { |
| result.set(new BuildRuleSuccess(AbstractCachingBuildRule.this)); |
| } |
| |
| @Override |
| public void onFailure(Throwable t) { |
| result.setException(t); |
| } |
| }, context.getExecutor()); |
| |
| return buildRuleResult; |
| } |
| |
| // This rule is not cached, so it needs to be built. Ultimately, buildRuleResult will be set |
| // with a BuildRuleResult (indicating success) or set with a Throwable (indicating a failure). |
| logger.info(String.format("[BUILDING %s]", getFullyQualifiedName())); |
| buildRuleResult = SettableFuture.create(); |
| |
| // Once all of the dependencies have been built, schedule this rule to build itself on the |
| // Executor associated with the BuildContext. |
| OnDepsBuiltCallback onDepsBuiltCallback = new OnDepsBuiltCallback(context); |
| Futures.addCallback(builtDeps, onDepsBuiltCallback, context.getExecutor()); |
| |
| Futures.addCallback(buildRuleResult, new FutureCallback<BuildRuleSuccess>() { |
| @Override |
| public void onSuccess(BuildRuleSuccess buildRuleSuccess) { |
| context.getEventBus().post(BuildEvents.finished( |
| AbstractCachingBuildRule.this, BuildRuleStatus.SUCCESS, CacheResult.MISS)); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| context.getEventBus().post(BuildEvents.finished( |
| AbstractCachingBuildRule.this, BuildRuleStatus.FAIL, CacheResult.MISS)); |
| } |
| }, context.getExecutor()); |
| |
| return buildRuleResult; |
| } |
| |
| /** |
| * Helper function for subclasses to create their lists of files for caching. |
| */ |
| protected static void addInputsToSortedSet(@Nullable String pathToDirectory, |
| ImmutableSortedSet.Builder<String> inputsToConsiderForCachingPurposes, |
| DirectoryTraverser traverser) { |
| if (pathToDirectory == null) { |
| return; |
| } |
| Set<String> files = DirectoryTraversers.getInstance().findFiles( |
| ImmutableSet.of(pathToDirectory), traverser); |
| inputsToConsiderForCachingPurposes.addAll(files); |
| } |
| |
| private class OnDepsBuiltCallback implements FutureCallback<List<BuildRuleSuccess>> { |
| |
| private final BuildContext context; |
| |
| private OnDepsBuiltCallback(BuildContext context) { |
| this.context = context; |
| } |
| |
| @Override |
| public void onSuccess(List<BuildRuleSuccess> results) { |
| final SettableFuture<BuildRuleSuccess> result = |
| (SettableFuture<BuildRuleSuccess>)buildRuleResult; |
| |
| ListenableFuture<BuildRuleSuccess> future = executeCommandsNowThatDepsAreBuilt(context); |
| Futures.addCallback( |
| future, |
| new FutureCallback<BuildRuleSuccess>() { |
| @Override |
| public void onSuccess(BuildRuleSuccess buildRuleSuccess) { |
| result.set(buildRuleSuccess); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| result.setException(throwable); |
| } |
| }, |
| context.getExecutor() |
| ); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| ((SettableFuture<BuildRuleSuccess>)buildRuleResult).setException(throwable); |
| } |
| } |
| |
| /** |
| * Execute the commands for this build rule. Requires all dependent rules are already built |
| * successfully. |
| */ |
| private ListenableFuture<BuildRuleSuccess> executeCommandsNowThatDepsAreBuilt( |
| final BuildContext context) { |
| |
| // Do the work to build this rule in a Callable so it can be scheduled on an Executor. |
| Callable<BuildRuleSuccess> callable = new Callable<BuildRuleSuccess>() { |
| @Override |
| public BuildRuleSuccess call() throws Exception { |
| AbstractCachingBuildRule buildRule = AbstractCachingBuildRule.this; |
| File output = getOutput(); |
| |
| // Try to fetch output from cache. |
| boolean fromCache = (output != null && artifactCache.fetch(getRuleKey(), output)); |
| if (!fromCache) { |
| // Get and run all of the commands. |
| List<Step> steps = buildInternal(context); |
| StepRunner stepRunner = context.getCommandRunner(); |
| for (Step step : steps) { |
| stepRunner.runStepForBuildTarget(step, getBuildTarget()); |
| } |
| } |
| |
| // Drop our cached output key, since it probably changed. |
| resetOutputKey(); |
| |
| // Write the success file. |
| buildRule.writeSuccessFile(); |
| |
| // Store output to cache. |
| if (output != null && !fromCache) { |
| artifactCache.store(getRuleKey(), output); |
| } |
| |
| // Return the object to represent the success of the build rule. |
| return new BuildRuleSuccess(buildRule); |
| } |
| }; |
| |
| return context.getCommandRunner().getListeningExecutorService().submit(callable); |
| } |
| |
| @Override |
| protected RuleKey.Builder ruleKeyBuilder() { |
| return super.ruleKeyBuilder() |
| .set("inputs", getInputs()); |
| } |
| |
| private String getPathToSuccessFile() { |
| return String.format("%s/%s/.success/%s", |
| BuckConstant.BIN_DIR, |
| getBuildTarget().getBasePath(), |
| getBuildTarget().getShortName()); |
| } |
| |
| /** |
| * Write out a file that represents that this build rule succeeded, as well as the inputs that |
| * were used. The last-modified time of this file, and its contents, will be used to determine |
| * whether this rule should be cached. |
| * @throws IOException |
| */ |
| private void writeSuccessFile() throws IOException { |
| String path = getPathToSuccessFile(); |
| Files.createParentDirs(new File(path)); |
| MoreFiles.writeLinesToFile(getSuccessFileStringsForBuildRules( |
| getRulesToConsiderForCaching(), false), path); |
| } |
| |
| /** |
| * When this method is run, all of its dependencies will have been built. |
| */ |
| abstract protected List<Step> buildInternal(BuildContext context) throws IOException; |
| } |