/* * Copyright 2015-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.keys; import com.facebook.buck.hashing.FileHashLoader; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildTargetSourcePath; import com.facebook.buck.rules.DependencyAggregation; import com.facebook.buck.rules.RuleKey; import com.facebook.buck.rules.RuleKeyAppendable; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.hash.HashCode; import java.io.IOException; import java.nio.file.Path; import java.util.function.Function; /** * A factory for generating input-based {@link RuleKey}s. * * @see SupportsInputBasedRuleKey */ public final class InputBasedRuleKeyFactory implements RuleKeyFactory<RuleKey> { private final RuleKeyFieldLoader ruleKeyFieldLoader; private final FileHashLoader fileHashLoader; private final SourcePathResolver pathResolver; private final SourcePathRuleFinder ruleFinder; private final long inputSizeLimit; private final SingleBuildRuleKeyCache<Result<RuleKey>> ruleKeyCache = new SingleBuildRuleKeyCache<>(); public InputBasedRuleKeyFactory( RuleKeyFieldLoader ruleKeyFieldLoader, FileHashLoader hashLoader, SourcePathResolver pathResolver, SourcePathRuleFinder ruleFinder, long inputSizeLimit) { this.ruleKeyFieldLoader = ruleKeyFieldLoader; this.fileHashLoader = hashLoader; this.pathResolver = pathResolver; this.ruleFinder = ruleFinder; this.inputSizeLimit = inputSizeLimit; } @VisibleForTesting public InputBasedRuleKeyFactory( int seed, FileHashLoader hashLoader, SourcePathResolver pathResolver, SourcePathRuleFinder ruleFinder) { this(new RuleKeyFieldLoader(seed), hashLoader, pathResolver, ruleFinder, Long.MAX_VALUE); } private Result<RuleKey> calculateBuildRuleKey(BuildRule buildRule) { Builder<HashCode> builder = newVerifyingBuilder(buildRule); ruleKeyFieldLoader.setFields(builder, buildRule, RuleKeyType.INPUT); return builder.buildResult(RuleKey::new); } private Result<RuleKey> calculateRuleKeyAppendableKey(RuleKeyAppendable appendable) { Builder<HashCode> subKeyBuilder = new Builder<>(RuleKeyBuilder.createDefaultHasher()); appendable.appendToRuleKey(subKeyBuilder); return subKeyBuilder.buildResult(RuleKey::new); } @Override public RuleKey build(BuildRule buildRule) { try { return ruleKeyCache.get(buildRule, this::calculateBuildRuleKey).getRuleKey(); } catch (RuntimeException e) { propagateIfSizeLimitException(e); throw e; } } private Result<RuleKey> buildAppendableKey(RuleKeyAppendable appendable) { return ruleKeyCache.get(appendable, this::calculateRuleKeyAppendableKey); } private void propagateIfSizeLimitException(Throwable throwable) { // At the moment, it is difficult to make SizeLimitException be a checked exception. Due to how // exceptions are currently handled (e.g. LoadingCache wraps them with ExecutionException), // we need to iterate through the cause chain to check if a SizeLimitException is wrapped. Throwables.getCausalChain(throwable) .stream() .filter(t -> t instanceof SizeLimiter.SizeLimitException) .findFirst() .ifPresent(Throwables::throwIfUnchecked); } private Builder<HashCode> newVerifyingBuilder(final BuildRule rule) { final Iterable<DependencyAggregation> aggregatedRules = Iterables.filter(rule.getBuildDeps(), DependencyAggregation.class); return new Builder<HashCode>(RuleKeyBuilder.createDefaultHasher()) { private boolean hasEffectiveDirectDep(BuildRule dep) { for (BuildRule aggregationRule : aggregatedRules) { if (aggregationRule.getBuildDeps().contains(dep)) { return true; } } return false; } // Construct the rule key, verifying that all the deps we saw when constructing it // are explicit dependencies of the rule. @Override public <RESULT> Result<RESULT> buildResult(Function<HashCode, RESULT> mapper) { Result<RESULT> result = super.buildResult(mapper); for (BuildRule usedDep : result.getDeps()) { Preconditions.checkState( rule.getBuildDeps().contains(usedDep) || hasEffectiveDirectDep(usedDep) || (rule instanceof AbstractBuildRule && ((AbstractBuildRule) rule).getTargetGraphOnlyDeps().contains(usedDep)), "%s: %s not in deps (%s)", rule.getBuildTarget(), usedDep.getBuildTarget(), rule.getBuildDeps()); } return result; } }; } /* package */ class Builder<RULE_KEY> extends RuleKeyBuilder<RULE_KEY> { private final ImmutableList.Builder<Iterable<BuildRule>> deps = ImmutableList.builder(); private final SizeLimiter sizeLimiter = new SizeLimiter(inputSizeLimit); public Builder(RuleKeyHasher<RULE_KEY> hasher) { super(ruleFinder, pathResolver, fileHashLoader, hasher); } @Override protected Builder<RULE_KEY> setAppendableRuleKey(RuleKeyAppendable appendable) { Result<RuleKey> result = InputBasedRuleKeyFactory.this.buildAppendableKey(appendable); deps.add(result.getDeps()); setAppendableRuleKey(result.getRuleKey()); return this; } @Override public Builder<RULE_KEY> setPath(Path absolutePath, Path ideallyRelative) throws IOException { // TODO(plamenko): this check should not be necessary, but otherwise some tests fail due to // FileHashLoader throwing NoSuchFileException which doesn't get correctly propagated. if (inputSizeLimit != Long.MAX_VALUE) { sizeLimiter.add(fileHashLoader.getSize(absolutePath)); } super.setPath(absolutePath, ideallyRelative); return this; } @Override protected Builder<RULE_KEY> setPath(ProjectFilesystem filesystem, Path relativePath) throws IOException { // TODO(plamenko): this check should not be necessary, but otherwise some tests fail due to // FileHashLoader throwing NoSuchFileException which doesn't get correctly propagated. if (inputSizeLimit != Long.MAX_VALUE) { sizeLimiter.add(fileHashLoader.getSize(filesystem, relativePath)); } super.setPath(filesystem, relativePath); return this; } @Override protected Builder<RULE_KEY> setNonHashingSourcePath(SourcePath sourcePath) { setNonHashingSourcePathDirectly(sourcePath); return this; } // Input-based rule keys are evaluated after all dependencies for a rule are available on // disk, and so we can always resolve the `Path` packaged in a `SourcePath`. We hash this, // rather than the rule key from it's `BuildRule`. @Override protected Builder<RULE_KEY> setSourcePath(SourcePath sourcePath) throws IOException { if (sourcePath instanceof BuildTargetSourcePath) { deps.add(ImmutableSet.of(ruleFinder.getRuleOrThrow((BuildTargetSourcePath) sourcePath))); // fall through and call setSourcePathDirectly as well } setSourcePathDirectly(sourcePath); return this; } // Rules supporting input-based rule keys should be described entirely by their `SourcePath` // inputs. If we see a `BuildRule` when generating the rule key, this is likely a break in // that contract, so check for that. @Override protected Builder<RULE_KEY> setBuildRule(BuildRule rule) { throw new IllegalStateException( String.format( "Input-based rule key builders cannot process build rules. " + "Was given %s to add to rule key.", rule)); } public <RESULT> Result<RESULT> buildResult(Function<RULE_KEY, RESULT> mapper) { return new Result<>(this.build(mapper), Iterables.concat(deps.build())); } } protected static class Result<RULE_KEY> { private final RULE_KEY ruleKey; private final Iterable<BuildRule> deps; public Result(RULE_KEY ruleKey, Iterable<BuildRule> deps) { this.ruleKey = ruleKey; this.deps = deps; } public RULE_KEY getRuleKey() { return ruleKey; } public Iterable<BuildRule> getDeps() { return deps; } } }