aswb/querysync/java/com/google/idea/blaze/qsync/query/QuerySummaryImpl.kt (393 lines of code) (raw):
/*
* Copyright 2023 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.idea.blaze.qsync.query
import com.google.common.annotations.VisibleForTesting
import com.google.devtools.build.lib.query2.proto.proto2api.Build
import com.google.idea.blaze.common.Interners
import com.google.idea.blaze.common.Label
import com.google.idea.blaze.common.Label.Companion.fromWorkspacePackageAndName
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.file.Path
/**
* Summaries the output from a `query` invocation into just the data needed by the rest of
* querysync.
*
*
* The main purpose of the summarized output is to allow the outputs from multiple `query`
* invocations to be combined. This enables delta updates to the project.
*
*
* If extra data from the `query` invocation is needed by later stages of sync, that data
* should be added to the [Query.Summary] proto and this code should be updated accordingly.
* The proto should remain a simple mapping of data from the build proto, i.e. no complex
* functionality should be added to this class. Non-trivial calculations based on the output of the
* query belong in [com.google.idea.blaze.qsync.BlazeQueryParser] instead.
*
*
* Instances of the the [Query.Summary] proto are maintained in memory so data should not
* be added to it unnecessarily.
*/
data class QuerySummaryImpl(
private val proto: Query.Summary,
) : QuerySummary {
override val isCompatibleWithCurrentPluginVersion get(): Boolean = proto.version == PROTO_VERSION
/** Do not generate toString, this object is too large */
override fun toString(): String {
return super.toString()
}
/**
* An opaque proto buffer to be serialized with the project state and re-create the [ ] using [QuerySummaryImpl.create].
*/
override fun protoForSerializationOnly(): Query.Summary = proto
private class StringIndexer {
private val strings: MutableMap<String, Int> = hashMapOf()
private val list: MutableList<String> = mutableListOf()
init {
strings.put("", 0)
list.add("")
}
fun ruleToStoredRule(r: QueryData.Rule): Query.StoredRule {
val builder = Query.StoredRule.newBuilder()
.setLabel(indexLabel(r.label))
.setRuleClass(index(r.ruleClass))
.addAllSources(indexLabels(r.sources))
.addAllDeps(indexLabels(r.deps))
.addAllIdlSources(indexLabels(r.idlSources))
.addAllRuntimeDeps(indexLabels(r.runtimeDeps))
.addAllResourceFiles(indexLabels(r.resourceFiles))
.setTestApp(index(r.testApp))
.setInstruments(index(r.instruments))
.setCustomPackage(index(r.customPackage))
.addAllHdrs(indexLabels(r.hdrs))
.addAllCopts(index(r.copts))
.addAllTags(index(r.tags))
.setMainClass(index(r.mainClass))
if (r.manifest != null) {
builder.setManifest(indexLabel(r.manifest))
}
return builder.build()
}
fun addStringAndGetIndex(s: String): Int {
list.add(intern(s))
return list.size - 1
}
fun index(s: String): Int {
return strings.getOrPut(s) { this.addStringAndGetIndex(s) }
}
fun indexLabel(l: Label): Query.StoredLabel {
return Query.StoredLabel.newBuilder()
.setWorkspace(index(l.workspace))
.setBuildPackage(index(l.buildPackage))
.setName(index(l.name))
.build()
}
fun indexLabels(labels: Collection<Label>): List<Query.StoredLabel> {
return labels.map { this.indexLabel(it) }
}
fun index(ss: Collection<String>): List<Int> {
return ss.map { strings.getOrPut(it) { this.addStringAndGetIndex(it) } }
}
fun list(): List<String> {
return list.toList()
}
fun sourceFileToStoredSourceFile(it: QueryData.SourceFile): Query.StoredSourceFile {
return Query.StoredSourceFile.newBuilder()
.setLabel(indexLabel(it.label))
.addAllSubinclude(indexLabels(it.subincliudes))
.build()
}
}
private class StringLookup(private val list: MutableList<String>) {
fun storedRuleToRule(r: Query.StoredRule): QueryData.Rule {
return QueryData.Rule(
label = lookupLabel(r.label),
ruleClass = lookupString(r.ruleClass),
sources = lookupLabels(r.sourcesList),
deps = lookupLabels(r.depsList),
idlSources = lookupLabels(r.idlSourcesList),
runtimeDeps = lookupLabels(r.runtimeDepsList),
resourceFiles = lookupLabels(r.resourceFilesList),
testApp = lookupString(r.testApp),
instruments = lookupString(r.instruments),
customPackage = lookupString(r.customPackage),
hdrs = lookupLabels(r.hdrsList),
copts = lookupStrings(r.coptsList),
tags = lookupStrings(r.tagsList),
mainClass = lookupString(r.mainClass),
manifest = if (r.hasManifest()) lookupLabel(r.manifest) else null,
testRule = if (r.hasTestRule()) lookupLabel(r.testRule) else null,
)
}
fun storedSourceFileToSourceFile(s: Query.StoredSourceFile): QueryData.SourceFile {
return QueryData.SourceFile(lookupLabel(s.label), lookupLabels(s.subincludeList))
}
fun lookupString(s: Int): String {
return list[s]
}
fun lookupStrings(ss: Collection<Int>): List<String> {
return ss.map { list[it] }
}
fun lookupLabel(l: Query.StoredLabel): Label {
return Label(
lookupString(l.workspace),
lookupString(l.buildPackage),
lookupString(l.name)
)
}
fun lookupLabels(ll: Collection<Query.StoredLabel>): List<Label> {
return ll.map { this.lookupLabel(it) }
}
}
override val queryStrategy: QuerySpec.QueryStrategy
get() {
return when (proto.getQueryStrategy()) {
Query.Summary.QueryStrategy.QUERY_STRATEGY_FILTERING_TO_KNOWN_AND_USED_TARGETS ->
QuerySpec.QueryStrategy.FILTERING_TO_KNOWN_AND_USED_TARGETS
Query.Summary.QueryStrategy.QUERY_STRATEGY_PLAIN_WITH_SAFE_FILTERS -> QuerySpec.QueryStrategy.PLAIN_WITH_SAFE_FILTERS
Query.Summary.QueryStrategy.QUERY_STRATEGY_PLAIN, Query.Summary.QueryStrategy.QUERY_STRATEGY_UNKNOWN -> QuerySpec.QueryStrategy.PLAIN
Query.Summary.QueryStrategy.UNRECOGNIZED -> throw IllegalStateException(proto.getQueryStrategy().toString())
}
}
/**
* Returns the map of source files included in the query output.
*
*
* This is a map of source target label to the [QueryData.SourceFile] proto representing it.
*/
override val sourceFilesMap: Map<Label, QueryData.SourceFile> by lazy {
val lookup = StringLookup(proto.stringStorage.indexedStringsList)
proto.sourceFilesList
.map { lookup.storedSourceFileToSourceFile(it) }
.associateBy { it.label }
}
/**
* Returns the map of rules included in the query output.
*
*
* This is a map of rule label to the [QueryData.Rule] proto representing it.
*/
override val rulesMap: Map<Label, QueryData.Rule> by lazy {
val lookup = StringLookup(proto.stringStorage.indexedStringsList)
proto.storedRulesList
.map { lookup.storedRuleToRule(it) }
.associateBy{it.label}
}
override val packagesWithErrors: Set<Path> by lazy {
proto.packagesWithErrorsList
.map { Label.of(it) }
.map { it.getBuildPackagePath() } // The packages are BUILD file labels.
.toSet()
}
/**
* Returns the set of build packages in the query output.
*
*
* The packages are workspace relative paths that contain a BUILD file.
*/
override val packages: PackageSet by lazy {
PackageSet(sourceFilesMap.keys.map { it.getBuildPackagePath() }.toSet() + packagesWithErrors)
}
/**
* Returns a map of .bzl file labels to BUILD file labels that include them.
*
*
* This is used to determine, for example, which build files include a given .bzl file.
*/
override val reverseSubincludeMap by lazy {
sourceFilesMap.entries.asSequence()
.flatMap { entry ->
entry.value.subincliudes.asSequence().map { subinclude -> subinclude to entry.key }
}
.groupBy({ it.first.toFilePath() }, { it.second.toFilePath() })
.mapValues { it.value.toSet() }
}
/**
* Returns the set of labels of all files includes from BUILD files.
*/
override val allBuildIncludedFiles: Set<Label> by lazy {
sourceFilesMap.values
.flatMap { it.subincliudes }
.toSet()
}
override val packagesWithErrorsCount: Int get() = proto.packagesWithErrorsCount
override val rulesCount: Int get() = proto.storedRulesCount
/**
* Builder for [QuerySummaryImpl]. This should be used when constructing a summary from a map of
* source files and rules. To construct one from a serialized proto, you should use [ ][QuerySummaryImpl.create] instead.
*/
class Builder internal constructor() {
private var indexer: StringIndexer = StringIndexer()
private val builder: Query.Summary.Builder = Query.Summary.newBuilder().setVersion(PROTO_VERSION)
fun putAllSourceFiles(sourceFileMap: Map<Label, QueryData.SourceFile>): Builder {
builder.addAllSourceFiles(sourceFileMap.values.map { indexer.sourceFileToStoredSourceFile(it) })
return this
}
fun putSourceFiles(sourceFile: QueryData.SourceFile): Builder {
builder.addSourceFiles(indexer.sourceFileToStoredSourceFile(sourceFile))
return this
}
fun putAllRules(rules: Collection<QueryData.Rule>): Builder {
builder.addAllStoredRules(rules.map { indexer.ruleToStoredRule(it) })
return this
}
fun putRules(rule: QueryData.Rule): Builder {
builder.addStoredRules(indexer.ruleToStoredRule(rule))
return this
}
fun putAllPackagesWithErrors(packagesWithErrors: Set<Path>): Builder {
packagesWithErrors // TODO: b/334110669 - Consider multi workspace-builds.
.asSequence()
.map {
fromWorkspacePackageAndName(
Label.ROOT_WORKSPACE,
it,
"BUILD"
)
}
.map { it.toString() }
.map { intern(it) }
.forEach { builder.addPackagesWithErrors(it) }
return this
}
fun putPackagesWithErrors(packageWithErrors: Path): Builder {
builder.addPackagesWithErrors(
intern(
fromWorkspacePackageAndName(Label.ROOT_WORKSPACE, packageWithErrors, "BUILD")
.toString()
)
)
return this
}
fun build(): QuerySummary {
builder.setStringStorage(
Query.StringStorage.newBuilder().addAllIndexedStrings(indexer.list())
)
return create(builder.build())
}
}
companion object {
/**
* The current version of the Query.Summary proto that this is compatible with. Any persisted
* protos with a different version embedded in them will be discarded.
*
*
* Whenever changing the logic in this class such that the Query.Summary proto contents will be
* different for the same input, this version should be incremented.
*/
@VisibleForTesting
const val PROTO_VERSION: Int = 12
// Compile-time dependency attributes, as they appear in streamed_proto output
private val DEPENDENCY_ATTRIBUTES: Set<String> =
setOf<String>( // android_local_test depends on junit implicitly using the _junit attribute.
"\$junit",
"deps",
"test_deps", // java_proto_library and java_lite_proto_library rules depend on the proto runtime
// library via these proto_toolchain attributes. In Starlark, the attribute names
// begin with an underscore instead of a colon (e.g., _aspect_java_proto_toolchain).
":aspect_java_proto_toolchain",
":aspect_proto_toolchain_for_javalite", // This is not strictly correct, as source files of rule with 'export' do not
// depend on exported targets.
"exports"
)
// Compile time dependency attributes scoped to specific rule kind, for cases where sync does not
// need to always need to traverse the attribute.
private val RULE_SCOPED_ATTRIBUTES: Map<String, Set<String>> =
mapOf<String, Set<String>>(
"\$toolchain" to
setOf(
"_java_grpc_library",
"_java_lite_grpc_library",
"kt_jvm_library_helper",
"android_library",
"kt_android_library"
)
)
// Runtime dependency attributes
private val RUNTIME_DEP_ATTRIBUTES: Set<String> =
setOf<String>( // From android_binary rules used in android_instrumentation_tests
"instruments", // From android_instrumentation_test rules
"test_app"
)
// Source attributes.
private val SRCS_ATTRIBUTES: Set<String> = setOf<String>(
"srcs", "java_srcs", "kotlin_srcs", "java_test_srcs", "kotlin_test_srcs", "common_srcs"
)
@JvmStatic
fun create(proto: Query.Summary): QuerySummary {
return QuerySummaryImpl(proto)
}
@JvmStatic
@Throws(IOException::class)
fun create(
queryStrategy: QuerySpec.QueryStrategy,
protoInputStream: InputStream
): QuerySummary {
// IMPORTANT: when changing the logic herein, you should also update PROTO_VERSION above.
// Failure to do so is likely to result in problems during a partial sync.
val sourceFileMap: MutableMap<Label, Query.StoredSourceFile> = hashMapOf()
val ruleMap: MutableMap<Label, Query.StoredRule> = hashMapOf()
val packagesWithErrors: MutableSet<String> = hashSetOf()
val indexer = StringIndexer()
var target: Build.Target?
while ((Build.Target.parseDelimitedFrom(protoInputStream).also { target = it }) != null) {
when (target!!.getType()) {
Build.Target.Discriminator.SOURCE_FILE -> {
val sourceFileLabel = Label.of(target.sourceFile.getName())
val sourceFile =
Query.StoredSourceFile.newBuilder()
.setLabel(indexer.indexLabel(sourceFileLabel))
.addAllSubinclude(indexer.indexLabels(target.sourceFile.subincludeList.map { Label.of(it) }))
.build()
sourceFileMap.put(sourceFileLabel, sourceFile)
if (target.sourceFile.packageContainsErrors) {
packagesWithErrors.add(intern(target.sourceFile.getName()))
}
}
Build.Target.Discriminator.RULE -> {
// TODO We don't need all rules types in the proto since many are not used later on.
// We could filter the rules here, or even create rule-specific proto messages to
// reduce the size of the output proto.
val rule =
Query.StoredRule.newBuilder()
.setRuleClass(indexer.index(target.rule.getRuleClass()))
val label = Label.of(target.rule.getName())
rule.setLabel(indexer.indexLabel(label))
for (a in target.rule.attributeList) {
val attributeName = intern(a.getName())
when {
SRCS_ATTRIBUTES.contains(attributeName) -> {
rule.addAllSources(indexer.indexLabels(a.asLabelListSafe()))
}
attributeName == "hdrs" -> {
rule.addAllHdrs(indexer.indexLabels(a.asLabelListSafe()))
}
attributeIsTrackedDependency(attributeName, target) -> {
rule.addAllDeps(indexer.indexLabels(a.asLabelListSafe()))
}
RUNTIME_DEP_ATTRIBUTES.contains(attributeName) -> {
rule.addAllRuntimeDeps(indexer.indexLabels(a.asLabelListSafe()))
}
attributeName == "idl_srcs" -> {
rule.addAllIdlSources(indexer.indexLabels(a.asLabelListSafe()))
}
attributeName == "resource_files" -> {
rule.addAllResourceFiles(indexer.indexLabels(a.asLabelListSafe()))
}
attributeName == "manifest" -> {
a.asLabelSafe()
?.let { rule.setManifest(indexer.indexLabel(it)) }
}
attributeName == "custom_package" -> {
rule.setCustomPackage(indexer.index((a.getStringValue())))
}
attributeName == "copts" -> {
rule.addAllCopts(indexer.index(a.stringListValueList))
}
attributeName == "tags" -> {
rule.addAllTags(indexer.index(a.stringListValueList))
}
attributeName == "main_class" -> {
rule.setMainClass(indexer.index(a.getStringValue()))
}
attributeName == "test_app" -> {
rule.setTestApp(indexer.index(a.getStringValue()))
}
attributeName == "instruments" -> {
rule.setInstruments(indexer.index(a.getStringValue()))
}
attributeName == "test_rule" -> {
a.asLabelSafe()
?.let { rule.setTestRule(indexer.indexLabel(it)) }
}
}
}
ruleMap.put(label, rule.build())
}
else -> {}
}
}
return create(
Query.Summary.newBuilder()
.setQueryStrategy(convertQueryStrategy(queryStrategy))
.setVersion(PROTO_VERSION)
.addAllSourceFiles(sourceFileMap.values)
.addAllStoredRules(ruleMap.values)
.setStringStorage(Query.StringStorage.newBuilder().addAllIndexedStrings(indexer.list()))
.addAllPackagesWithErrors(packagesWithErrors)
.build()
)
}
private fun convertQueryStrategy(queryStrategy: QuerySpec.QueryStrategy): Query.Summary.QueryStrategy {
return when (queryStrategy) {
QuerySpec.QueryStrategy.PLAIN -> Query.Summary.QueryStrategy.QUERY_STRATEGY_PLAIN
QuerySpec.QueryStrategy.FILTERING_TO_KNOWN_AND_USED_TARGETS -> Query.Summary.QueryStrategy.QUERY_STRATEGY_FILTERING_TO_KNOWN_AND_USED_TARGETS
QuerySpec.QueryStrategy.PLAIN_WITH_SAFE_FILTERS -> Query.Summary.QueryStrategy.QUERY_STRATEGY_PLAIN_WITH_SAFE_FILTERS
}
}
private fun attributeIsTrackedDependency(
attributeName: String,
target: Build.Target
): Boolean {
if (DEPENDENCY_ATTRIBUTES.contains(attributeName)) {
return true
}
return (RULE_SCOPED_ATTRIBUTES[attributeName] ?: return false).contains(target.rule.getRuleClass())
}
@JvmStatic
@Throws(IOException::class)
fun create(querySpecStrategy: QuerySpec.QueryStrategy, protoFile: File): QuerySummary {
return create(querySpecStrategy, BufferedInputStream(FileInputStream(protoFile)))
}
@JvmStatic
fun newBuilder(): Builder = Builder()
private fun intern(s: String): String {
return Interners.STRING.intern(s)
}
private fun intern(list: List<String>): List<String> = list.map { Interners.STRING.intern(it) }
}
}
private fun Build.Attribute.asLabelListSafe(): List<Label> {
return when (this.type) {
Build.Attribute.Discriminator.LABEL ->
listOfNotNull(this.stringValue.takeUnless { it.isNullOrEmpty() }?.let { Label.of(it) })
Build.Attribute.Discriminator.LABEL_LIST ->
this.stringListValueList.map { Label.of(it) }
else ->
emptyList()
}
}
private fun Build.Attribute.asLabelSafe(): Label? {
return when (this.type) {
Build.Attribute.Discriminator.LABEL ->
this.stringValue.takeUnless { it.isNullOrEmpty() }?.let { Label.of(it) }
else ->
null
}
}