blob: b14012d73d3d1d0f91b933bd00c924411654d3cc [file] [log] [blame]
/*
* 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.util.FileHashCache;
import com.facebook.buck.util.hash.AppendingHasher;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
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.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* RuleKey encapsulates regimented computation of SHA-1 keys that incorporate all BuildRule state
* relevant to idempotency. The RuleKey.Builder API conceptually implements the construction of an
* ordered map, and the key/val pairs are digested using an internal serialization that guarantees
* a 1:1 mapping for each distinct vector of keys
* <header,k1,...,kn> in RuleKey.builder(header).set(k1, v1) ... .set(kn, vn).build().
* <p>
* Note carefully that in order to reliably avoid accidental collisions, each RuleKey schema, as
* defined by the key vector, must have a distinct header. Otherwise it is possible (if unlikely)
* for serialized value data to alias serialized key data, with the result being identical RuleKeys
* for differing input. In practical terms this means that each BuildRule implementer should specify
* a distinct header, and that for all RuleKeys built with a particular header, the sequence
* of set() calls should be identical, even if values are missing. The set() methods specifically
* handle null values to accommodate this regime.
*/
public class RuleKey {
private final HashCode hashCode;
private RuleKey(HashCode hashCode) {
this.hashCode = Preconditions.checkNotNull(hashCode);
}
/**
* @param hashString string that conforms to the contract of the return value of
* {@link com.google.common.hash.HashCode#toString()}.
*/
public RuleKey(String hashString) {
this(HashCode.fromString(hashString));
}
public HashCode getHashCode() {
return hashCode;
}
/** @return the {@link #toString} of the hash code that underlies this RuleKey. */
@Override
public String toString() {
return getHashCode().toString();
}
/**
* Takes a string and uses it to construct a {@link RuleKey}.
* <p>
* Is likely particularly useful with {@link Optional#transform(Function)}.
*/
public static final Function<String, RuleKey> TO_RULE_KEY =
new Function<String, RuleKey>() {
@Override
public RuleKey apply(String hash) {
return new RuleKey(hash);
}
};
@Override
public boolean equals(Object obj) {
if (!(obj instanceof RuleKey)) {
return false;
}
RuleKey that = (RuleKey)obj;
return Objects.equal(this.getHashCode(), that.getHashCode());
}
@Override
public int hashCode() {
return this.getHashCode().hashCode();
}
/**
* Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s inputs.
*/
public static Builder builder(BuildRule rule, FileHashCache hashCache) {
Builder builder = new Builder(rule, hashCache)
.set("name", rule.getFullyQualifiedName())
// Keyed as "buck.type" rather than "type" in case a build rule has its own "type" argument.
.set("buck.type", rule.getType().getName());
return builder;
}
public static class Builder {
@VisibleForTesting
static final byte SEPARATOR = '\0';
private static final Logger logger = Logger.getLogger(Builder.class.getName());
private final BuildRule rule;
private final Hasher hasher;
private final FileHashCache hashCache;
@Nullable private List<String> logElms;
private Builder(BuildRule rule, FileHashCache hashCache) {
this.rule = Preconditions.checkNotNull(rule);
this.hasher = new AppendingHasher(Hashing.sha1(), /* numHashers */ 2);
this.hashCache = Preconditions.checkNotNull(hashCache);
if (logger.isLoggable(Level.INFO)) {
this.logElms = Lists.newArrayList();
}
}
private Builder feed(byte[] bytes) {
hasher.putBytes(bytes);
return this;
}
private Builder separate() {
hasher.putByte(SEPARATOR);
return this;
}
private Builder setKey(String sectionLabel) {
if (logElms != null) {
logElms.add(String.format(":key(%s):", sectionLabel));
}
return separate().feed(sectionLabel.getBytes()).separate();
}
private Builder setVal(@Nullable String s) {
if (s != null) {
if (logElms != null) {
logElms.add(String.format("string(\"%s\"):", s));
}
feed(s.getBytes());
}
return separate();
}
private Builder setVal(boolean b) {
if (logElms != null) {
logElms.add(String.format("boolean(\"%s\"):", b ? "true" : "false"));
}
return feed((b ? "t" : "f").getBytes()).separate();
}
private Builder setVal(long value) {
if (logElms != null) {
logElms.add(String.format("long(\"%s\"):", value));
}
hasher.putLong(value);
separate();
return this;
}
private Builder setVal(@Nullable RuleKey ruleKey) {
if (ruleKey != null) {
if (logElms != null) {
logElms.add(String.format("ruleKey(sha1=%s):", ruleKey));
}
feed(ruleKey.toString().getBytes());
}
return separate();
}
public Builder set(String key, @Nullable String val) {
return setKey(key).setVal(val);
}
public Builder set(String key, Optional<String> val) {
return set(key, val.isPresent() ? val.get() : null);
}
public Builder set(String key, boolean val) {
return setKey(key).setVal(val);
}
public Builder set(String key, long val) {
return setKey(key).setVal(val);
}
public Builder set(String key, @Nullable RuleKey val) {
return setKey(key).setVal(val);
}
public Builder set(String key, @Nullable BuildRule val) throws IOException {
return setKey(key).setVal(val != null ? val.getRuleKey() : null);
}
public Builder set(String key, @Nullable ImmutableList<SourceRoot> val) {
setKey(key);
if (val != null) {
for (SourceRoot root : val) {
setVal(root.getName());
}
}
return separate();
}
public Builder set(String key, @Nullable List<String> val) {
setKey(key);
if (val != null) {
for (String s : val) {
setVal(s);
}
}
return separate();
}
/**
* @param inputs is an {@link Iterator} rather than an {@link Iterable} because {@link Path}
* implements {@link Iterable} and we want to protect against passing a single {@link Path}
* instead of multiple {@link Path}s.
*/
public Builder setInputs(String key, Iterator<Path> inputs) throws IOException {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(inputs);
setKey(key);
while (inputs.hasNext()) {
Path input = inputs.next();
HashCode sha1 = hashCache.get(input);
if (sha1 == null) {
throw new RuntimeException("No SHA for " + input);
}
setVal(sha1.toString());
}
return separate();
}
public Builder setSourcePaths(String key, @Nullable ImmutableSortedSet<SourcePath> val) {
setKey(key);
if (val != null) {
for (SourcePath path : val) {
setVal(path.asReference());
}
}
return separate();
}
/**
* @param val The fully qualified name (not the {@link RuleKey}) of each rule in this collection
* will contribute to the {@link RuleKey} being built by this builder.
*/
public Builder setRuleNames(String key, ImmutableSortedSet<? extends BuildRule> val) {
return set(key, FluentIterable
.from(val)
.transform(new Function<BuildRule, String>() {
@Override
public String apply(BuildRule buildRule) {
return buildRule.getFullyQualifiedName();
}
})
.toList());
}
public Builder set(String key, @Nullable ImmutableSortedSet<? extends BuildRule> val)
throws IOException {
setKey(key);
if (val != null) {
for (BuildRule buildRule : val) {
setVal(buildRule.getRuleKey());
}
}
return separate();
}
public Builder set(String key, @Nullable ImmutableSet<String> val) {
setKey(key);
if (val != null) {
ImmutableSortedSet<String> sortedValues = ImmutableSortedSet.copyOf(val);
for (String value : sortedValues) {
setVal(value);
}
}
return separate();
}
public static class RuleKeyPair {
private final RuleKey totalRuleKey;
private final RuleKey ruleKeyWithoutDeps;
private RuleKeyPair(RuleKey totalRuleKey, RuleKey ruleKeyWithoutDeps) {
this.totalRuleKey = Preconditions.checkNotNull(totalRuleKey);
this.ruleKeyWithoutDeps = Preconditions.checkNotNull(ruleKeyWithoutDeps);
}
public RuleKey getTotalRuleKey() {
return totalRuleKey;
}
public RuleKey getRuleKeyWithoutDeps() {
return ruleKeyWithoutDeps;
}
}
public RuleKeyPair build() throws IOException {
RuleKey ruleKeyWithoutDeps = new RuleKey(hasher.hash());
// Now introduce the deps into the RuleKey.
setKey("deps");
// Note that getDeps() returns an ImmutableSortedSet, so the order will be stable.
for (BuildRule buildRule : rule.getDeps()) {
setVal(buildRule.getRuleKey());
}
separate();
RuleKey totalRuleKey = new RuleKey(hasher.hash());
if (logElms != null) {
logger.info(String.format("RuleKey %s=%s", totalRuleKey, Joiner.on("").join(logElms)));
}
return new RuleKeyPair(totalRuleKey, ruleKeyWithoutDeps);
}
}
}