blob: 164f43d41fc4944fad050f647a68a874b7946075 [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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
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.HashCodes;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
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().
*
* 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 implements Comparable<RuleKey> {
private static final String nonIdempotentRuleString = builder().nonIdempotent().build().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) {
if (nonIdempotentRuleString.equals(hash)) {
return builder().nonIdempotent().build();
}
return new RuleKey(hash);
}
};
// TODO(mbolin): Do not let this be @Nullable anymore.
@Nullable private final HashCode 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(HashCodes.fromBytes(hashStringToBytes(hashString)));
}
private static byte[] hashStringToBytes(String hashString) {
byte[] bytes = new byte[hashString.length() / 2];
for (int i = 0; i < hashString.length(); i += 2) {
char highOrderBits = hashString.charAt(i);
char lowOrderBits = hashString.charAt(i + 1);
bytes[i / 2] = (byte)
((Byte.parseByte(String.valueOf(highOrderBits), 16) << 4) +
Byte.parseByte(String.valueOf(lowOrderBits), 16));
}
return bytes;
}
private RuleKey(HashCode hashCode) {
this.hashCode = hashCode;
}
@Nullable
public HashCode getHashCode() {
return hashCode;
}
public boolean isIdempotent() {
return (hashCode != null);
}
/**
* Non-idempotent RuleKeys are normally output as strings of 'x' characters, but when comparing
* two sets of RuleKeys in textual form it is necessary to mangle one of the two sets, so that
* non-idempotent RuleKeys are never considered equal.
*/
public String toString(boolean mangleNonIdempotent) {
if (!isIdempotent()) {
return new String (new char[Hashing.sha1().bits() / 4]).replace("\0",
mangleNonIdempotent ? "y" : "x");
}
return hashCode.toString();
}
@Override
public String toString() {
return toString(false);
}
/**
* Order non-idempotent RuleKeys as less than all idempotent RuleKeys.
*
* non-idempotent < idempotent
*/
@Override
public int compareTo(RuleKey other) {
if (!isIdempotent()) {
if (!other.isIdempotent()) {
return 0;
}
return -1;
} else if (!other.isIdempotent()) {
return 1;
}
return ByteBuffer.wrap(hashCode.asBytes()).compareTo(ByteBuffer.wrap(other.hashCode.asBytes()));
}
/**
* Treat non-idempotent RuleKeys as unequal to everything, including other non-idempotent
* RuleKeys.
*/
@Override
public boolean equals(Object that) {
if (!(that instanceof RuleKey)) {
return false;
}
RuleKey other = (RuleKey) that;
if (!isIdempotent() || !other.isIdempotent()) {
return false;
}
return (compareTo(other) == 0);
}
@Override
public int hashCode() {
if (!isIdempotent()) {
return 0;
}
return super.hashCode();
}
/**
* Helper method used to avoid memoizing non-idempotent RuleKeys.
*/
@Nullable
public static RuleKey filter(RuleKey ruleKey) {
if (!ruleKey.isIdempotent()) {
return null;
}
return ruleKey;
}
public static Builder builder() {
return new Builder();
}
/**
* Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s input
* arguments.
*/
public static Builder builder(BuildRule rule) {
return builder(rule, /* includeDeps */ true);
}
/**
* Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s input
* arguments <em>except</em> for its <code>deps</doe>.
*/
public static Builder builderWithoutDeps(BuildRule rule) {
return builder(rule, /* includeDeps */ false);
}
private static Builder builder(BuildRule rule, boolean includeDeps) {
Builder builder = new Builder()
.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());
if (includeDeps) {
builder.setKey("deps");
// Note that getDeps() returns an ImmutableSortedSet, so the order will be stable.
for (BuildRule buildRule : rule.getDeps()) {
builder.setVal(buildRule.getRuleKey());
}
}
builder.separate();
return builder;
}
public static class Builder {
private static final String BUCK_VERSION_UID_KEY = "buck.version_uid";
@VisibleForTesting
static final String buckVersionUID = System.getProperty(BUCK_VERSION_UID_KEY, "N/A");
@VisibleForTesting
static final byte SEPARATOR = '\0';
private static final Logger logger = Logger.getLogger(Builder.class.getName());
private final Hasher hasher;
private boolean idempotent;
@Nullable private List<String> logElms;
private Builder() {
hasher = Hashing.sha1().newHasher();
idempotent = true;
if (logger.isLoggable(Level.INFO)) {
logElms = Lists.newArrayList();
}
setBuckVersionUID();
}
private Builder(String header) {
this();
setHeader(header);
}
private Builder feed(byte[] bytes) {
hasher.putBytes(bytes);
return this;
}
private Builder separate() {
hasher.putByte(SEPARATOR);
return this;
}
private void setBuckVersionUID() {
if (logElms != null) {
logElms.add(String.format("buckVersionUID(%s):", buckVersionUID));
}
feed(buckVersionUID.getBytes()).separate();
}
private void setHeader(String header) {
if (logElms != null) {
logElms.add(String.format("header(%s):", header));
}
feed(header.getBytes()).separate();
}
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 File file) {
if (file != null) {
// Compute a separate SHA-1 for the file contents and feed that into messageDigest rather
// than the file contents, in order to avoid the overhead of escaping SEPARATOR in the file
// content.
try {
InputSupplier<? extends InputStream> inputSupplier = Files.newInputStreamSupplier(file);
HashCode fileSha1 = ByteStreams.hash(inputSupplier, Hashing.sha1());
if (logElms != null) {
logElms.add(String.format("file(path=\"%s\", sha1=%s):", file.getPath(),
fileSha1.toString()));
}
feed(fileSha1.asBytes());
} catch (IOException e) {
// The file is nonexistent/unreadable; generate a RuleKey that prevents accidental
// caching.
if (logElms != null) {
logElms.add(String.format("file(path=\"%s\", sha1=random):", file.getPath()));
}
nonIdempotent();
}
}
return 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(@Nullable RuleKey ruleKey) {
if (ruleKey != null) {
if (logElms != null) {
logElms.add(String.format("%sruleKey(sha1=%s):",
ruleKey.isIdempotent() ? "" : "non-idempotent ", ruleKey.toString()));
}
feed(ruleKey.toString().getBytes())
.mergeIdempotence(ruleKey.isIdempotent());
}
return separate();
}
public Builder set(String key, @Nullable File val) {
return setKey(key).setVal(val);
}
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, @Nullable RuleKey val) {
return setKey(key).setVal(val);
}
public Builder set(String key, @Nullable BuildRule val) {
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();
}
public Builder setInputs(String key, @Nullable Iterable<InputRule> val) {
setKey(key);
if (val != null) {
for (InputRule inputRule : val) {
setVal(inputRule.getRuleKey());
}
}
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();
}
public Builder set(String key, @Nullable ImmutableSortedSet<? extends BuildRule> val) {
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();
}
/**
* The idempotence of the RuleKey to be built is false if this method is ever called with a
* false argument.
*/
public Builder mergeIdempotence(boolean idempotence) {
if (!idempotence) {
idempotent = false;
}
return this;
}
/**
* Non-idempotent RuleKeys can be built by calling nonIdempotent() at any building stage. This
* method is intended for generation of RuleKeys to be associated with non-cacheable BuildRule
* results.
*/
public Builder nonIdempotent() {
if (logElms != null) {
logElms.add(String.format("nonIdempotent()"));
}
return mergeIdempotence(false);
}
public RuleKey build() {
RuleKey ruleKey = idempotent ? new RuleKey(hasher.hash()) : new RuleKey((HashCode)null);
if (logElms != null) {
logger.info(String.format("%sRuleKey %s=%s",
ruleKey.isIdempotent() ? "" : "non-idempotent ", ruleKey.toString(),
Joiner.on("").join(logElms)));
}
return ruleKey;
}
}
}