blob: 4b4b11c0d1d7c56bb0669f15c0cd9d062ebb1c9b [file] [log] [blame]
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.query2.query.output;
import com.google.common.base.Preconditions;
import com.google.common.flogger.GoogleLogger;
import com.google.common.hash.HashFunction;
import com.google.common.hash.HashingOutputStream;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.AttributeFormatter;
import com.google.devtools.build.lib.packages.BuildType.SelectorList;
import com.google.devtools.build.lib.packages.LabelPrinter;
import com.google.devtools.build.lib.packages.RawAttributeMapper;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.query2.proto.proto2api.Build;
import com.google.protobuf.CodedOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Contains the logic for condensing the various properties of rules that contribute to their
* "affectedness" into a simple hash value. The resulting hash may be compared across queries to
* tell if a rule has changed in a potentially meaningful way.
*/
class SyntheticAttributeHashCalculator {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private SyntheticAttributeHashCalculator() {}
/**
* Returns a hash of various properties of a rule which might contribute to the rule's
* "affectedness". This includes, but is not limited to, attribute values and error-state.
*
* @param rule The rule instance to calculate the hash for.
* @param serializedAttributes Any available attribute which have already been serialized. This is
* an optimization to avoid re-serializing attributes internally.
* @param extraDataForAttrHash Extra data to add to the hash.
*/
static String compute(
Rule rule,
Map<Attribute, Build.Attribute> serializedAttributes,
Object extraDataForAttrHash,
HashFunction hashFunction) {
HashingOutputStream hashingOutputStream =
new HashingOutputStream(hashFunction, ByteStreams.nullOutputStream());
CodedOutputStream codedOut = CodedOutputStream.newInstance(hashingOutputStream);
RuleClass ruleClass = rule.getRuleClassObject();
if (ruleClass.isStarlark()) {
try {
codedOut.writeByteArrayNoTag(
Preconditions.checkNotNull(ruleClass.getRuleDefinitionEnvironmentDigest(), rule));
} catch (IOException e) {
throw new IllegalStateException("Unexpected IO failure writing to digest stream", e);
}
}
RawAttributeMapper rawAttributeMapper = RawAttributeMapper.of(rule);
for (Attribute attr : rule.getAttributes()) {
String attrName = attr.getName();
if (attrName.equals("generator_location")) {
// generator_location can be ignored for the purpose of telling if a rule has changed.
continue;
}
Object valueToHash = rawAttributeMapper.getRawAttributeValue(rule, attr);
if (valueToHash instanceof ComputedDefault) {
// ConfiguredDefaults need special handling to detect changes in evaluated values.
ComputedDefault computedDefault = (ComputedDefault) valueToHash;
if (!computedDefault.dependencies().isEmpty()) {
// TODO(b/29038463): We're skipping computed defaults that depend on other configurable
// attributes because there currently isn't a way to evaluate such a computed default;
// there isn't *one* value it evaluates to.
continue;
}
try {
valueToHash = computedDefault.getDefault(rawAttributeMapper);
} catch (IllegalArgumentException e) {
// TODO(mschaller): Catching IllegalArgumentException isn't ideal. It's thrown by
// AbstractAttributeMapper#get if the attribute's type doesn't match its value, which
// would happen if a ComputedDefault function accessed an attribute whose value was
// configurable. We check whether the ComputedDefault declared any configurable
// attribute dependencies above, but someone could make a mistake and fail to declare
// something. There's no mechanism that enforces correct declaration right now.
// This allows us to recover from such an error by skipping an attribute, as opposed to
// crashing.
logger.atWarning().log(
"Recovering from failed evaluation of ComputedDefault attribute value: %s", e);
continue;
}
}
Build.Attribute attrPb;
if (valueToHash instanceof SelectorList<?> || !serializedAttributes.containsKey(attr)) {
// We didn't already serialize the attribute or it's a SelectorList. Latter may
// have been flattened while we want the full representation, so we start from scratch.
attrPb =
AttributeFormatter.getAttributeProto(
attr,
valueToHash,
/* explicitlySpecified= */ false, // We care about value, not how it was set.
/* encodeBooleanAndTriStateAsIntegerAndString= */ false,
LabelPrinter.legacy());
} else {
attrPb = serializedAttributes.get(attr);
}
try {
attrPb.writeTo(codedOut);
} catch (IOException e) {
throw new IllegalStateException("Unexpected IO failure writing to digest stream", e);
}
}
try {
// Rules can be considered changed when the containing package goes in/out of error.
codedOut.writeBoolNoTag(rule.getPackage().containsErrors());
} catch (IOException e) {
throw new IllegalStateException("Unexpected IO failure writing to digest stream", e);
}
try {
// Include a summary of any package-wide data that applies to this target (e.g. custom make
// variables aka `vardef`).
codedOut.writeStringNoTag((String) extraDataForAttrHash);
} catch (IOException e) {
throw new IllegalStateException("Unexpected IO failure writing to digest stream", e);
}
try {
// Flush coded out to make sure all bytes make it to the underlying digest stream.
codedOut.flush();
} catch (IOException e) {
throw new IllegalStateException("Unexpected flush failure", e);
}
return BaseEncoding.base64().encode(hashingOutputStream.hash().asBytes());
}
}