-
Notifications
You must be signed in to change notification settings - Fork 439
mbt(bazel): add a streamed_proto decoder for bazel query output #8703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main-v2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| #!/usr/bin/env bash | ||
| # Regenerates the `bazel query --output=streamed_proto` fixture consumed by | ||
| # `tests.bazel.BazelTargetsProtoDumpRealOutputSuite`: | ||
| # | ||
| # tests/unit/src/test/resources/bazel/rules-scala-fullinfo-fragments.pb | ||
| # | ||
| # Run it from INSIDE a bazelbuild/rules_scala checkout (commit 156e65c / | ||
| # v7.2.5-5-g156e65c, Bazel 7.7.1). It does no validation — it just runs the | ||
| # importer's exact full-information query over the nine pinned targets and | ||
| # writes the raw binary stream. | ||
| # | ||
| # Usage (from within the rules_scala repo): | ||
| # /path/to/metals/bin/regenerate-bazel-proto-fixture.sh > rules-scala-fullinfo-fragments.pb | ||
| # /path/to/metals/bin/regenerate-bazel-proto-fixture.sh /path/to/metals/tests/unit/src/test/resources/bazel/rules-scala-fullinfo-fragments.pb | ||
| set -euo pipefail | ||
|
|
||
| # The nine hand-picked targets: six rules plus a SOURCE_FILE, a GENERATED_FILE | ||
| # and a PACKAGE_GROUP, chosen to cover the decoder's edge cases with no overlap. | ||
| TARGETS="\ | ||
| //third_party/dependency_analyzer/src/main:scala_version \ | ||
| //src/java/io/bazel/rulesscala/scalac/reporter:reporter \ | ||
| //third_party/dependency_analyzer/src/main/io/bazel/rulesscala/dependencyanalyzer/compiler:dep_reporting_compiler \ | ||
| //src/java/io/bazel/rulesscala/scalac:scalac_files \ | ||
| //src/java/io/bazel/rulesscala/scalac:scalac \ | ||
| //third_party/dependency_analyzer/src/test:scalac_dependency_test \ | ||
| //java_stub_template/file:file.txt \ | ||
| //scala:libPlaceHolderClassToCreateEmptyJarForScalaImport.jar \ | ||
| @bazel_tools//src/main/cpp/util:ijar" | ||
|
|
||
| # The importer's production query flags (see BazelQuery.scala) plus | ||
| # `--proto:locations=false` to keep the fixture compact. | ||
| # `--keep_going` exits 3 on partial success, which is expected here, | ||
| # so don't let `set -e` trip on it. | ||
| # stdout is the raw binary; a path arg (if given) receives it, else it | ||
| # goes to this script's stdout for the caller to redirect. | ||
| run() { | ||
| bazel query \ | ||
| --output=streamed_proto \ | ||
| --proto:flatten_selects=false \ | ||
| --proto:output_rule_attrs=srcs,scalacopts,javacopts,scala_version,jars,srcjar \ | ||
| --proto:locations=false \ | ||
| --keep_going \ | ||
| "set(${TARGETS})" | ||
| } | ||
|
|
||
| if [[ $# -ge 1 ]]; then | ||
| run > "$1" || { rc=$?; [[ $rc -eq 3 ]] || exit $rc; } | ||
| echo "Wrote $(wc -c < "$1") bytes to $1" >&2 | ||
| else | ||
| run || { rc=$?; [[ $rc -eq 3 ]] || exit $rc; } | ||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // A trimmed subset of Bazel's `src/main/protobuf/build.proto` for the MBT | ||
| // importer. Field NUMBERS must stay identical to build.proto — that is what the | ||
| // wire format depends on. | ||
| // Based on | ||
| // https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/build.proto | ||
| syntax = "proto2"; | ||
|
|
||
| package metals_bazel_query; | ||
|
|
||
| option java_package = "scala.meta.internal.metals.mbt.importer.bazelproto"; | ||
| option java_outer_classname = "BazelQueryProto"; | ||
|
maksymilianrozanski marked this conversation as resolved.
|
||
|
|
||
| message Attribute { | ||
| message SelectorEntry { | ||
| optional string label = 1; | ||
| optional string string_value = 3; | ||
| repeated string string_list_value = 6; | ||
| } | ||
|
|
||
| message Selector { | ||
| repeated SelectorEntry entries = 1; | ||
| } | ||
|
|
||
| message SelectorList { | ||
| repeated Selector elements = 2; | ||
| } | ||
|
|
||
| optional string name = 1; | ||
| optional string string_value = 5; | ||
| repeated string string_list_value = 6; | ||
| optional SelectorList selector_list = 21; | ||
| } | ||
|
|
||
| message Rule { | ||
| optional string name = 1; | ||
| optional string rule_class = 2; | ||
| repeated Attribute attribute = 4; | ||
| repeated string rule_input = 5; | ||
| repeated string rule_output = 6; | ||
| } | ||
|
|
||
| message Target { | ||
| optional Rule rule = 2; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||||||||||||||
| package scala.meta.internal.metals.mbt.importer | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import scala.collection.mutable | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Per-target Scala-version recovery from unflattened `select()` srcs. | ||||||||||||||||||||||||||||||
| object BazelBuildSrcs { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| case class TargetSrcs( | ||||||||||||||||||||||||||||||
| unconditional: Set[String], | ||||||||||||||||||||||||||||||
| byVersion: Map[String, Set[String]], | ||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||
| def activeFor(scalaVersion: Option[String]): Set[String] = | ||||||||||||||||||||||||||||||
| unconditional ++ scalaVersion.flatMap(byVersion.get).getOrElse(Set.empty) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
it took me a while to understand, so this is maybe a bit better? |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| case class InactiveSource(version: String, originTarget: String) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Highest inactive-branch version wins; ties broken by smallest origin label. | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a bit more why use it, why do we have inactive sources in bazel. |
||||||||||||||||||||||||||||||
| def inactiveSources( | ||||||||||||||||||||||||||||||
| srcsByTarget: Map[String, TargetSrcs], | ||||||||||||||||||||||||||||||
| scalaVersionByTarget: Map[String, Option[String]], | ||||||||||||||||||||||||||||||
| ): Map[String, InactiveSource] = { | ||||||||||||||||||||||||||||||
| val byTarget = srcsByTarget.filter { case (target, _) => | ||||||||||||||||||||||||||||||
| scalaVersionByTarget.contains(target) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| val active = byTarget.flatMap { case (target, srcs) => | ||||||||||||||||||||||||||||||
| srcs.activeFor(scalaVersionByTarget.getOrElse(target, None)) | ||||||||||||||||||||||||||||||
| }.toSet | ||||||||||||||||||||||||||||||
| val candidatesByLabel = | ||||||||||||||||||||||||||||||
| mutable.Map.empty[String, mutable.Set[(String, String)]] | ||||||||||||||||||||||||||||||
| for { | ||||||||||||||||||||||||||||||
| (target, srcs) <- byTarget | ||||||||||||||||||||||||||||||
| (version, srcLabels) <- srcs.byVersion | ||||||||||||||||||||||||||||||
| srcLabel <- srcLabels | ||||||||||||||||||||||||||||||
| if !active.contains(srcLabel) | ||||||||||||||||||||||||||||||
| } candidatesByLabel.getOrElseUpdate(srcLabel, mutable.Set.empty) += | ||||||||||||||||||||||||||||||
| (version -> target) | ||||||||||||||||||||||||||||||
| candidatesByLabel.flatMap { case (srcLabel, candidates) => | ||||||||||||||||||||||||||||||
| BazelScalaVersions | ||||||||||||||||||||||||||||||
| .maxVersion(candidates.map { case (version, _) => version }) | ||||||||||||||||||||||||||||||
| .map { version => | ||||||||||||||||||||||||||||||
| val origin = candidates.collect { case (`version`, target) => | ||||||||||||||||||||||||||||||
| target | ||||||||||||||||||||||||||||||
| }.min | ||||||||||||||||||||||||||||||
| srcLabel -> InactiveSource(version, origin) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }.toMap | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private val scalaVersionKey = """:scala_version_(\d+_\d+_\d+)""".r | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Non-`scala_version` branches (literals, defaults) count as always compiled. | ||||||||||||||||||||||||||||||
| def parseSrcs(rule: BazelRule): TargetSrcs = { | ||||||||||||||||||||||||||||||
| val unconditional = mutable.Set.empty[String] | ||||||||||||||||||||||||||||||
| val byVersion = mutable.Map.empty[String, mutable.Set[String]] | ||||||||||||||||||||||||||||||
| for (branch <- rule.branches("srcs")) { | ||||||||||||||||||||||||||||||
| branchVersion(branch.label) match { | ||||||||||||||||||||||||||||||
| case Some(version) => | ||||||||||||||||||||||||||||||
| byVersion.getOrElseUpdate( | ||||||||||||||||||||||||||||||
| version, | ||||||||||||||||||||||||||||||
| mutable.Set.empty, | ||||||||||||||||||||||||||||||
| ) ++= branch.values | ||||||||||||||||||||||||||||||
| case None => unconditional ++= branch.values | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| TargetSrcs( | ||||||||||||||||||||||||||||||
| unconditional.toSet, | ||||||||||||||||||||||||||||||
| byVersion.map { case (k, v) => k -> v.toSet }.toMap, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private def branchVersion(label: Option[String]): Option[String] = | ||||||||||||||||||||||||||||||
| label.flatMap { l => | ||||||||||||||||||||||||||||||
| scalaVersionKey.findFirstMatchIn(l).map(_.group(1).replace('_', '.')) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package scala.meta.internal.metals.mbt.importer | ||
|
|
||
| // A `bazel query` rule reduced to the fields the importer reads. `attributes` | ||
| // keeps each `select()` unflattened as a list of branches. | ||
| case class BazelRule( | ||
| name: String, | ||
| ruleClass: Option[String], | ||
| ruleInputs: List[String], | ||
| ruleOutputs: List[String], | ||
| attributes: Map[String, List[BazelSelectBranch]], | ||
| ) { | ||
|
|
||
| def branches(attribute: String): List[BazelSelectBranch] = | ||
| attributes.getOrElse(attribute, Nil) | ||
|
|
||
| def flattenedStrings(attribute: String): List[String] = | ||
| branches(attribute).flatMap(_.values).filter(_.nonEmpty) | ||
| } | ||
|
|
||
| // `label` is the `config_setting` guarding a `select()` branch, or `None` for a | ||
| // plain value; `values` holds a scalar as a single-element list. | ||
| case class BazelSelectBranch(label: Option[String], values: List[String]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package scala.meta.internal.metals.mbt.importer | ||
|
|
||
| import scala.util.Failure | ||
| import scala.util.Success | ||
| import scala.util.Try | ||
|
|
||
| import scala.meta.internal.semver.SemVer | ||
|
|
||
| object BazelScalaVersions { | ||
|
|
||
| // Highest SemVer-parseable version; `scala_version` is a free-form STRING, so | ||
| // unparseable values are logged and skipped. | ||
| def maxVersion(versions: Iterable[String]): Option[String] = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can probable move it to ScalaVersions class, which handles this already. |
||
| versions.toSeq.distinct | ||
| .flatMap { version => | ||
| Try(SemVer.Version.fromString(version)) match { | ||
| case Success(parsed) => Some(version -> parsed) | ||
| case Failure(_) => | ||
| scribe.warn( | ||
| s"bazel-mbt: could not parse Scala version '$version'; ignoring it" | ||
| ) | ||
| None | ||
| } | ||
| } | ||
| .maxByOption { case (_, parsed) => parsed } | ||
| .map { case (version, _) => version } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MAybe it makes sense to put it into test resources?