/*
* 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 static com.facebook.buck.rules.keys.RuleKeyHasher.Container;
import static com.facebook.buck.rules.keys.RuleKeyHasher.Wrapper;
import static com.facebook.buck.rules.keys.RuleKeyScopedHasher.ContainerScope;
import static com.facebook.buck.rules.keys.RuleKeyScopedHasher.Scope;
import com.facebook.buck.hashing.FileHashLoader;
import com.facebook.buck.io.ArchiveMemberPath;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.Either;
import com.facebook.buck.rules.ArchiveMemberSourcePath;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.BuildTargetSourcePath;
import com.facebook.buck.rules.NonHashableSourcePathContainer;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.RuleKeyAppendable;
import com.facebook.buck.rules.RuleKeyObjectSink;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.SourceRoot;
import com.facebook.buck.rules.SourceWithFlags;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.sha1.Sha1HashCode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.function.Function;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* A base implementation for rule key builders.
*
* <p>{@link RuleKeyFactory} classes create concrete instances of this class and use them to produce
* rule keys. Concrete implementations may tweak behavior of the builder, and at the very minimum
* should implement {@link #setAppendableRuleKey(RuleKeyAppendable)}, and {@link
* #setBuildRule(BuildRule)}.
*
* <p>This class implements {@link RuleKeyObjectSink} interface which is the primary mechanism of
* how {@link RuleKeyFactory} and {@link RuleKeyAppendable} classes feed the builder.
*
* <p>Each element fed to the builder is a (key, value) pair. Keys are always simple strings,
* typically the name of a field annotated with {@link AddToRuleKey}. Values on the other hand may
* be complex types that are resolved recursively. For instance, a list of elements gets serialized
* by serializing each element of the list in order, and finally serializing the list token along
* with the length of the list. Similarly for other containers and wrappers.
*
* <p>There is an exception to the above rule of how containers and wrappers get serialized. Namely,
* they only get serialized if at least one of their elements gets serialized. This is to support
* concrete rule key builders that ignore some elements, or handle them differently. For example,
* several concrete builders handle {@link SourcePath} elements in a special way.
*
* @param <RULE_KEY> - the actual type that the builder produces (e.g. {@code HashCode}).
*/
public abstract class RuleKeyBuilder<RULE_KEY> implements RuleKeyObjectSink {
private static final Logger logger = Logger.get(RuleKeyBuilder.class);
private final SourcePathRuleFinder ruleFinder;
private final SourcePathResolver resolver;
private final FileHashLoader hashLoader;
private final CountingRuleKeyHasher<RULE_KEY> hasher;
private final RuleKeyScopedHasher<RULE_KEY> scopedHasher;
public RuleKeyBuilder(
SourcePathRuleFinder ruleFinder,
SourcePathResolver resolver,
FileHashLoader hashLoader,
RuleKeyHasher<RULE_KEY> hasher) {
this.ruleFinder = ruleFinder;
this.resolver = resolver;
this.hashLoader = hashLoader;
this.hasher = new CountingRuleKeyHasher<>(hasher);
this.scopedHasher = new RuleKeyScopedHasher<>(this.hasher);
}
@VisibleForTesting
RuleKeyScopedHasher<RULE_KEY> getScopedHasher() {
return this.scopedHasher;
}
public static RuleKeyHasher<HashCode> createDefaultHasher() {
RuleKeyHasher<HashCode> hasher = new GuavaRuleKeyHasher(Hashing.sha1().newHasher());
if (logger.isVerboseEnabled()) {
hasher =
new ForwardingRuleKeyHasher<HashCode, String>(hasher, new StringRuleKeyHasher()) {
@Override
protected void onHash(HashCode firstHash, String secondHash) {
logger.verbose("RuleKey %s=%s", firstHash, secondHash);
}
};
}
return hasher;
}
@Override
public final RuleKeyBuilder<RULE_KEY> setReflectively(String key, @Nullable Object val) {
try (Scope keyScope = scopedHasher.keyScope(key)) {
return setReflectively(val);
}
}
/** Recursively serializes the value. Serialization of the key is handled outside. */
protected RuleKeyBuilder<RULE_KEY> setReflectively(@Nullable Object val) {
if (val instanceof RuleKeyAppendable) {
return setAppendableRuleKey((RuleKeyAppendable) val);
}
if (val instanceof BuildRule) {
return setBuildRule((BuildRule) val);
}
if (val instanceof Supplier) {
try (Scope containerScope = scopedHasher.wrapperScope(Wrapper.SUPPLIER)) {
Object newVal = ((Supplier<?>) val).get();
return setReflectively(newVal);
}
}
if (val instanceof Optional) {
Object o = ((Optional<?>) val).orElse(null);
try (Scope wraperScope = scopedHasher.wrapperScope(Wrapper.OPTIONAL)) {
return setReflectively(o);
}
}
if (val instanceof Either) {
Either<?, ?> either = (Either<?, ?>) val;
if (either.isLeft()) {
try (Scope wraperScope = scopedHasher.wrapperScope(Wrapper.EITHER_LEFT)) {
return setReflectively(either.getLeft());
}
} else {
try (Scope wraperScope = scopedHasher.wrapperScope(Wrapper.EITHER_RIGHT)) {
return setReflectively(either.getRight());
}
}
}
// Check to see if we're dealing with a collection of some description.
// Note {@link java.nio.file.Path} implements "Iterable", so we explicitly exclude it here.
if (val instanceof Iterable && !(val instanceof Path)) {
try (ContainerScope containerScope = scopedHasher.containerScope(Container.LIST)) {
for (Object element : (Iterable<?>) val) {
try (Scope elementScope = containerScope.elementScope()) {
setReflectively(element);
}
}
return this;
}
}
if (val instanceof Iterator) {
Iterator<?> iterator = (Iterator<?>) val;
try (ContainerScope containerScope = scopedHasher.containerScope(Container.LIST)) {
while (iterator.hasNext()) {
try (Scope elementScope = containerScope.elementScope()) {
setReflectively(iterator.next());
}
}
}
return this;
}
if (val instanceof Map) {
if (!(val instanceof SortedMap || val instanceof ImmutableMap)) {
logger.warn(
"Adding an unsorted map to the rule key. "
+ "Expect unstable ordering and caches misses: %s",
val);
}
try (ContainerScope containerScope = scopedHasher.containerScope(Container.MAP)) {
for (Map.Entry<?, ?> entry : ((Map<?, ?>) val).entrySet()) {
try (Scope elementScope = containerScope.elementScope()) {
setReflectively(entry.getKey());
}
try (Scope elementScope = containerScope.elementScope()) {
setReflectively(entry.getValue());
}
}
}
return this;
}
if (val instanceof Path) {
throw new HumanReadableException(
"It's not possible to reliably disambiguate Paths. They are disallowed from rule keys");
}
if (val instanceof SourcePath) {
try {
return setSourcePath((SourcePath) val);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (val instanceof NonHashableSourcePathContainer) {
SourcePath sourcePath = ((NonHashableSourcePathContainer) val).getSourcePath();
return setNonHashingSourcePath(sourcePath);
}
if (val instanceof SourceWithFlags) {
SourceWithFlags source = (SourceWithFlags) val;
try (ContainerScope containerScope = scopedHasher.containerScope(Container.TUPLE)) {
try (Scope elementScope = containerScope.elementScope()) {
setSourcePath(source.getSourcePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
try (Scope elementScope = containerScope.elementScope()) {
setReflectively(source.getFlags());
}
}
return this;
}
return setSingleValue(val);
}
/**
* Implementations should ask their factories to compute the rule key for the {@link BuildRule}
* and call {@link #setBuildRuleKey(RuleKey)} on it.
*/
protected abstract RuleKeyBuilder<RULE_KEY> setBuildRule(BuildRule rule);
/** To be called from {@link #setBuildRule(BuildRule)}. */
protected final RuleKeyBuilder<RULE_KEY> setBuildRuleKey(RuleKey ruleKey) {
try (Scope wraperScope = scopedHasher.wrapperScope(Wrapper.BUILD_RULE)) {
return setSingleValue(ruleKey);
}
}
/**
* Implementations should ask their factories to compute the rule key for the {@link
* RuleKeyAppendable} and call {@link #setAppendableRuleKey(RuleKey)} on it.
*/
protected abstract RuleKeyBuilder<RULE_KEY> setAppendableRuleKey(RuleKeyAppendable appendable);
/** To be called from {@link #setAppendableRuleKey(RuleKeyAppendable)}. */
protected final RuleKeyBuilder<RULE_KEY> setAppendableRuleKey(RuleKey ruleKey) {
try (Scope wraperScope = scopedHasher.wrapperScope(Wrapper.APPENDABLE)) {
return setSingleValue(ruleKey);
}
}
/**
* Implementations may just forward to {@link #setSourcePathDirectly}. However, they will
* typically want to handle {@link BuildTargetSourcePath} explicitly. See also {@link
* #setSourcePathAsRule}.
*/
protected abstract RuleKeyBuilder<RULE_KEY> setSourcePath(SourcePath sourcePath)
throws IOException;
/**
* To be called from {@link #setSourcePath(SourcePath)}. Note that this implementation handles
* {@link BuildTargetSourcePath} same as a {@link PathSourcePath} pointing to the output of that
* target. Implementations may change this behavior by handling {@link BuildTargetSourcePath}
* explicitly in {@link #setSourcePath(SourcePath)} instead of calling this method. See also
* {@link #setSourcePathAsRule}.
*/
protected final RuleKeyBuilder<RULE_KEY> setSourcePathDirectly(SourcePath sourcePath)
throws IOException {
if (sourcePath instanceof BuildTargetSourcePath) {
return setPath(resolver.getFilesystem(sourcePath), resolver.getRelativePath(sourcePath));
} else if (sourcePath instanceof PathSourcePath) {
Path ideallyRelativePath = resolver.getIdeallyRelativePath(sourcePath);
if (ideallyRelativePath.isAbsolute()) {
return setPath(
resolver.getAbsolutePath(sourcePath), resolver.getIdeallyRelativePath(sourcePath));
} else {
return setPath(resolver.getFilesystem(sourcePath), ideallyRelativePath);
}
} else if (sourcePath instanceof ArchiveMemberSourcePath) {
return setArchiveMemberPath(
resolver.getFilesystem(sourcePath), resolver.getRelativeArchiveMemberPath(sourcePath));
} else {
throw new UnsupportedOperationException(
"Unrecognized SourcePath implementation: " + sourcePath.getClass());
}
}
/**
* To be called from {@link #setSourcePath(SourcePath)} in case {@link BuildTargetSourcePath}
* should be handled as a build rule. This method hashes the given {@link BuildTargetSourcePath}
* and invokes {@link #setBuildRule(BuildRule)} on the associated rule.
*/
protected final RuleKeyBuilder<RULE_KEY> setSourcePathAsRule(BuildTargetSourcePath sourcePath) {
try (ContainerScope containerScope = scopedHasher.containerScope(Container.TUPLE)) {
try (Scope elementScope = containerScope.elementScope()) {
hasher.putBuildTargetSourcePath(sourcePath);
}
try (Scope elementScope = containerScope.elementScope()) {
setBuildRule(ruleFinder.getRuleOrThrow(sourcePath));
}
}
return this;
}
// Paths get added as a combination of the file name and file hash. If the path is absolute
// then we only include the file name (assuming that it represents a tool of some kind
// that's being used for compilation or some such). This does mean that if a user renames a
// file without changing the contents, we have a cache miss. We're going to assume that this
// doesn't happen that often in practice.
@Override
public RuleKeyBuilder<RULE_KEY> setPath(Path absolutePath, Path ideallyRelative)
throws IOException {
// TODO(simons): Enable this precondition once setPath(Path) has been removed.
// Preconditions.checkState(absolutePath.isAbsolute());
if (ideallyRelative.isAbsolute()) {
logger.warn(
"Attempting to add absolute path to rule key. Only using file name: %s", ideallyRelative);
ideallyRelative = ideallyRelative.getFileName();
}
hasher.putPath(ideallyRelative, hashLoader.get(absolutePath));
return this;
}
protected RuleKeyBuilder<RULE_KEY> setPath(ProjectFilesystem filesystem, Path relativePath)
throws IOException {
Preconditions.checkArgument(!relativePath.isAbsolute());
hasher.putPath(relativePath, hashLoader.get(filesystem, relativePath));
return this;
}
private RuleKeyBuilder<RULE_KEY> setArchiveMemberPath(
ProjectFilesystem filesystem, ArchiveMemberPath relativeArchiveMemberPath)
throws IOException {
Preconditions.checkArgument(!relativeArchiveMemberPath.isAbsolute());
hasher.putArchiveMemberPath(
relativeArchiveMemberPath, hashLoader.get(filesystem, relativeArchiveMemberPath));
return this;
}
/** Implementations may just forward to {@link #setNonHashingSourcePathDirectly}. */
protected abstract RuleKeyBuilder<RULE_KEY> setNonHashingSourcePath(SourcePath sourcePath);
protected final RuleKeyBuilder<RULE_KEY> setNonHashingSourcePathDirectly(SourcePath sourcePath) {
if (sourcePath instanceof BuildTargetSourcePath) {
hasher.putNonHashingPath(resolver.getRelativePath(sourcePath).toString());
} else if (sourcePath instanceof PathSourcePath) {
hasher.putNonHashingPath(resolver.getRelativePath(sourcePath).toString());
} else if (sourcePath instanceof ArchiveMemberSourcePath) {
hasher.putNonHashingPath(resolver.getRelativeArchiveMemberPath(sourcePath).toString());
} else {
throw new UnsupportedOperationException(
"Unrecognized SourcePath implementation: " + sourcePath.getClass());
}
return this;
}
private RuleKeyBuilder<RULE_KEY> setSingleValue(@Nullable Object val) {
if (val == null) { // Null value first
hasher.putNull();
} else if (val instanceof Boolean) { // JRE types
hasher.putBoolean((boolean) val);
} else if (val instanceof Enum) {
hasher.putString(String.valueOf(val));
} else if (val instanceof Number) {
hasher.putNumber((Number) val);
} else if (val instanceof String) {
hasher.putString((String) val);
} else if (val instanceof Pattern) {
hasher.putPattern((Pattern) val);
} else if (val instanceof BuildRuleType) { // Buck types
hasher.putBuildRuleType((BuildRuleType) val);
} else if (val instanceof RuleKey) {
hasher.putRuleKey((RuleKey) val);
} else if (val instanceof BuildTarget) {
hasher.putBuildTarget((BuildTarget) val);
} else if (val instanceof SourceRoot) {
hasher.putSourceRoot((SourceRoot) val);
} else if (val instanceof Sha1HashCode) {
hasher.putSha1((Sha1HashCode) val);
} else if (val instanceof byte[]) {
hasher.putBytes((byte[]) val);
} else {
throw new RuntimeException("Unsupported value type: " + val.getClass());
}
return this;
}
/** Builds the rule key hash. */
public final RULE_KEY build() {
return hasher.hash();
}
/** A convenience method that builds the rule key hash and transforms it with a mapper. */
public final <RESULT> RESULT build(Function<RULE_KEY, RESULT> mapper) {
return mapper.apply(build());
}
}