diff --git a/bazel_rules/bsp4bazel.bzl b/bazel_rules/bsp4bazel.bzl index 8ff03a0..4ebc468 100644 --- a/bazel_rules/bsp4bazel.bzl +++ b/bazel_rules/bsp4bazel.bzl @@ -1,5 +1,5 @@ load("//private:load_tool.bzl", "load_tool") -load("//private:bsp_workspace_deps.bzl", _bsp_workspace_deps = "bsp_workspace_deps") +load("//private:bsp_workspace_info.bzl", _bsp_workspace_info = "bsp_workspace_info") # <--- Updated automatically by release job _bsp4bazel_version = "0.0.30" @@ -26,8 +26,8 @@ def bsp4bazel_setup(): _bsp4bazel_load("linux-x86") _bsp4bazel_load("macos-x86") -def bsp_workspace_deps(name = "bsp_workspace_deps"): - if (name != "bsp_workspace_deps"): - fail("name must be 'bsp_workspace_deps'") +def bsp_workspace_info(name = "bsp_workspace_info"): + if (name != "bsp_workspace_info"): + fail("name must be 'bsp_workspace_info'") - _bsp_workspace_deps(name = name) \ No newline at end of file + _bsp_workspace_info(name = name) diff --git a/bazel_rules/bsp_target_info_aspect.bzl b/bazel_rules/bsp_target_info_aspect.bzl index 2c16885..b0c0941 100644 --- a/bazel_rules/bsp_target_info_aspect.bzl +++ b/bazel_rules/bsp_target_info_aspect.bzl @@ -1,6 +1,4 @@ load("@io_bazel_rules_scala//scala:semanticdb_provider.bzl", "SemanticdbInfo") -load("@io_bazel_rules_scala_config//:config.bzl", "SCALA_VERSION") -load("@io_bazel_rules_scala//scala/private/toolchain_deps:toolchain_deps.bzl", "find_deps_info_on") def _collect_src_files(srcs): return [ @@ -25,21 +23,6 @@ def bsp_target_info_aspect_impl(target, ctx): toolchain = ctx.toolchains["@io_bazel_rules_scala//scala:toolchain_type"] classpath = [file.path for file in target[JavaInfo].transitive_compile_time_jars.to_list()] - compile_classpath = [ - file - for deps in find_deps_info_on(ctx, "@io_bazel_rules_scala//scala:toolchain_type", "scala_compile_classpath").deps - for file in deps[JavaInfo].compile_jars.to_list() - ] - - # Why not use the class_jar in SemanticdbInfo? Because it's a string, not a File, so we can't pass - # pass it to outputs. - # https://github.com/bazelbuild/rules_scala/issues/1527 - semanticdb_classpath = [ - file - for deps in find_deps_info_on(ctx, "@io_bazel_rules_scala//scala:toolchain_type", "semanticdb").deps - for file in deps[JavaInfo].compile_jars.to_list() - ] - src_files = [] if hasattr(ctx.rule.attr, "srcs"): src_files = _collect_src_files(ctx.rule.attr.srcs) @@ -47,14 +30,11 @@ def bsp_target_info_aspect_impl(target, ctx): src_files = src_files + _collect_src_files(ctx.rule.attr.resources) output_struct = struct( - scala_version = SCALA_VERSION, scalac_options = toolchain.scalacopts, classpath = classpath, - scala_compiler_jars = [file.path for file in compile_classpath], srcs = src_files, target_label = str(target.label), semanticdb_target_root = target[SemanticdbInfo].target_root, - semanticdb_pluginjar = [file.path for file in semanticdb_classpath], ) json_output_file = ctx.actions.declare_file("%s_bsp_target_info.json" % target.label.name) diff --git a/bazel_rules/private/bsp_workspace_info.bzl b/bazel_rules/private/bsp_workspace_info.bzl new file mode 100644 index 0000000..8c1a639 --- /dev/null +++ b/bazel_rules/private/bsp_workspace_info.bzl @@ -0,0 +1,62 @@ +load("@io_bazel_rules_scala//scala/private/toolchain_deps:toolchain_deps.bzl", "find_deps_info_on") +load("@io_bazel_rules_scala_config//:config.bzl", "SCALA_VERSION") + +def _copy_files(ctx, files): + output_files = [] + + for file in files: + output_file = ctx.actions.declare_file(ctx.attr.name + "_" + file.basename) + + ctx.actions.run_shell( + inputs = [file], + outputs = [output_file], + command = "cp {src} {out}".format(src = file.path, out = output_file.path), + ) + + output_files.append(output_file) + + return output_files + +def _bsp_workspace_info_impl(ctx): + """ + Outputs metadata about the BSP workspace, as well as copying depenendies to output (so that downstream tools can use them) + + Args: + ctx: The context object. + + Returns: + A list of dependencies for the BSP workspace. + """ + + compile_classpath = [ + file + for deps in find_deps_info_on(ctx, "@io_bazel_rules_scala//scala:toolchain_type", "scala_compile_classpath").deps + for file in deps[JavaInfo].compile_jars.to_list() + ] + + # Why not use the class_jar in SemanticdbInfo? Because it's a string, not a File, so we can't pass + # pass it to outputs. + # https://github.com/bazelbuild/rules_scala/issues/1527 + semanticdb_classpath = [ + file + for deps in find_deps_info_on(ctx, "@io_bazel_rules_scala//scala:toolchain_type", "semanticdb").deps + for file in deps[JavaInfo].compile_jars.to_list() + ] + + scalac_output_files = _copy_files(ctx, compile_classpath) + semanticdb_output_files = _copy_files(ctx, semanticdb_classpath) + + json_output_file = ctx.actions.declare_file(ctx.attr.name + ".json") + output_struct = struct( + scala_version = SCALA_VERSION, + scalac_deps = [file.path for file in scalac_output_files], + semanticdb_dep = [file.path for file in semanticdb_output_files][0], + ) + ctx.actions.write(json_output_file, json.encode_indent(output_struct)) + + return [DefaultInfo(files = depset(scalac_output_files + semanticdb_output_files + [json_output_file]))] + +bsp_workspace_info = rule( + implementation = _bsp_workspace_info_impl, + toolchains = ["@io_bazel_rules_scala//scala:toolchain_type"], +) diff --git a/bazel_rules/test/rules.test.scala b/bazel_rules/test/rules.test.scala index 20c01d9..6a394fd 100644 --- a/bazel_rules/test/rules.test.scala +++ b/bazel_rules/test/rules.test.scala @@ -21,16 +21,17 @@ class BazelRulesTest extends munit.FunSuite: assert(os.exists(dir / "bazel-bin" / "src" / "example" / "foo" / "foo_bsp_target_info.json")) } - test(s"should output bsp_workspace_deps.json for $dir") { + test(s"should output bsp_workspace_info.json for $dir") { os.proc( dir / "bazel", "build", - "//:bsp_workspace_deps", + "//:bsp_workspace_info", ).call(cwd = dir) - assert(os.exists(dir / "bazel-bin" / "bsp_workspace_deps.json")) + assert(os.exists(dir / "bazel-bin" / "bsp_workspace_info.json")) - val json = os.read(dir / "bazel-bin" / "bsp_workspace_deps.json") + val json = os.read(dir / "bazel-bin" / "bsp_workspace_info.json") + assert(json.contains("2.12.18"), "No scala version") assert(json.contains("semanticdb-scalac"), "No semanticdb dep") assert(json.contains("scala-reflect"), "No scala-reflect dep") assert(json.contains("scala-library"), "No scala-library dep") diff --git a/examples/simple-no-errors/BUILD b/examples/simple-no-errors/BUILD index 7045f4d..50c2cb8 100644 --- a/examples/simple-no-errors/BUILD +++ b/examples/simple-no-errors/BUILD @@ -1,3 +1,3 @@ -load("@bsp4bazel-rules//:bsp4bazel.bzl", "bsp_workspace_deps") +load("@bsp4bazel-rules//:bsp4bazel.bzl", "bsp_workspace_info") -bsp_workspace_deps() \ No newline at end of file +bsp_workspace_info() diff --git a/examples/simple-with-errors/BUILD b/examples/simple-with-errors/BUILD index 7045f4d..50c2cb8 100644 --- a/examples/simple-with-errors/BUILD +++ b/examples/simple-with-errors/BUILD @@ -1,3 +1,3 @@ -load("@bsp4bazel-rules//:bsp4bazel.bzl", "bsp_workspace_deps") +load("@bsp4bazel-rules//:bsp4bazel.bzl", "bsp_workspace_info") -bsp_workspace_deps() \ No newline at end of file +bsp_workspace_info() diff --git a/src/main/scala/bazeltools/bsp4bazel/BazelBspServer.scala b/src/main/scala/bazeltools/bsp4bazel/BazelBspServer.scala index 98d4bd7..024c2d4 100644 --- a/src/main/scala/bazeltools/bsp4bazel/BazelBspServer.scala +++ b/src/main/scala/bazeltools/bsp4bazel/BazelBspServer.scala @@ -72,11 +72,13 @@ class Bsp4BazelServer( state <- stateRef.get workspaceRoot <- state.workspaceRoot.asIO runner <- state.bspTaskRunner.asIO + workspaceInfo <- runner.workspaceInfo targets <- buildTargets(logger, workspaceRoot, runner) _ <- stateRef.update(s => s.copy( targets = Some(targets), - targetSourceMap = Bsp4BazelServer.TargetSourceMap(targets) + targetSourceMap = Bsp4BazelServer.TargetSourceMap(targets), + workspaceInfo = Some(workspaceInfo) ) ) yield () @@ -85,8 +87,9 @@ class Bsp4BazelServer( for _ <- logger.info("workspace/buildTargets") state <- stateRef.get - targets <- state.targets.asIO - yield WorkspaceBuildTargetsResult(targets.map(_.asBuildTarget)) + workspaceInfo <- state.workspaceInfo.asIO + bspTargets <- state.targets.asIO + yield WorkspaceBuildTargetsResult(bspTargets.map(_.asBuildTarget(workspaceInfo))) def buildTargetInverseSources( params: InverseSourcesParams @@ -107,6 +110,7 @@ class Bsp4BazelServer( for _ <- logger.info("buildTarget/scalacOptions") state <- stateRef.get + workspaceInfo <- state.workspaceInfo.asIO bspTargets <- state.targets.asIO yield val select = params.targets.toSet @@ -115,7 +119,7 @@ class Bsp4BazelServer( filtered.size == params.targets.size, "Some of the requested targets didn't exist. Shouldn't be possible" ) - ScalacOptionsResult(filtered.map(_.asScalaOptionItem)) + ScalacOptionsResult(filtered.map(_.asScalaOptionItem(workspaceInfo))) private def buildTargetScalacOption( target: BuildTargetIdentifier @@ -298,11 +302,12 @@ object Bsp4BazelServer: currentErrors: List[PublishDiagnosticsParams], workspaceRoot: Option[Path], bspTaskRunner: Option[BspTaskRunner], - targets: Option[List[BspTaskRunner.BspTarget]] + targets: Option[List[BspTaskRunner.BspTarget]], + workspaceInfo: Option[BspTaskRunner.WorkspaceInfo] ) def defaultState: ServerState = - ServerState(Bsp4BazelServer.TargetSourceMap.empty, Nil, None, None, None) + ServerState(Bsp4BazelServer.TargetSourceMap.empty, Nil, None, None, None, None) def create( client: BspClient, diff --git a/src/main/scala/bazeltools/bsp4bazel/protocol/Models.scala b/src/main/scala/bazeltools/bsp4bazel/protocol/Models.scala index 1d64404..ae107ec 100644 --- a/src/main/scala/bazeltools/bsp4bazel/protocol/Models.scala +++ b/src/main/scala/bazeltools/bsp4bazel/protocol/Models.scala @@ -629,3 +629,12 @@ case class CompileReport( ) object CompileReport: given Codec[CompileReport] = deriveCodec[CompileReport] + + +object CommonCodecs: + given pathCodec: Codec[Path] = Codec.from( + Decoder[String].emap { s => + Either.catchNonFatal(Paths.get(s)).leftMap(_.getMessage) + }, + Encoder[String].contramap(_.toString) + ) \ No newline at end of file diff --git a/src/main/scala/bazeltools/bsp4bazel/runner/BspTaskRunner.scala b/src/main/scala/bazeltools/bsp4bazel/runner/BspTaskRunner.scala index e5d4ed2..20cde39 100644 --- a/src/main/scala/bazeltools/bsp4bazel/runner/BspTaskRunner.scala +++ b/src/main/scala/bazeltools/bsp4bazel/runner/BspTaskRunner.scala @@ -76,21 +76,37 @@ object BspTaskRunner: ) ) - case class BspTarget( - id: BuildTargetIdentifier, - workspaceRoot: Path, - info: BspTargetInfo + case class WorkspaceInfo( + scalaVersion: String, + scalacDeps: List[Path], + semanticdbDep: Path ): - - private def majorVersion(version: String): String = + def majorScalaVersion: String = val SemVer = """(\d+)\.(\d+)\.(\d+).*""".r - version match - case SemVer("2", minor, patch) => + scalaVersion match + case SemVer("2", minor, _) => List("2", minor).mkString(".") case SemVer("3", _, _) => "3" - def asBuildTarget: BuildTarget = + object WorkspaceInfo: + + import bazeltools.bsp4bazel.protocol.CommonCodecs.pathCodec + + given workspaceInfoDecoder: Decoder[WorkspaceInfo] = + Decoder.forProduct3( + "scala_version", + "scalac_deps", + "semanticdb_dep" + )(WorkspaceInfo.apply) + + case class BspTarget( + id: BuildTargetIdentifier, + workspaceRoot: Path, + info: BspTargetInfo + ): + + def asBuildTarget(workspaceInfo: WorkspaceInfo): BuildTarget = BuildTarget( id = id, displayName = Some(id.uri.getPath), @@ -98,16 +114,15 @@ object BspTaskRunner: tags = List("library"), capabilities = BuildTargetCapabilities(true, false, false, false), languageIds = List("scala"), - // TODO dependencies = Nil, dataKind = Some("scala"), Some( ScalaBuildTarget( scalaOrganization = "org.scala-lang", - scalaVersion = info.scalaVersion, - scalaBinaryVersion = majorVersion(info.scalaVersion), + scalaVersion = workspaceInfo.scalaVersion, + scalaBinaryVersion = workspaceInfo.majorScalaVersion, platform = ScalaPlatform.JVM, - jars = info.scalaCompileJars.map(p => + jars = workspaceInfo.scalacDeps.map(p => UriFactory.fileUri(workspaceRoot.resolve(p)) ), jvmBuildTarget = None @@ -115,13 +130,13 @@ object BspTaskRunner: ) ) - def asScalaOptionItem: ScalacOptionsItem = + def asScalaOptionItem(workspaceInfo: WorkspaceInfo): ScalacOptionsItem = ScalacOptionsItem( target = id, // NB: Metals looks for these parameters to be set specifically. They don't really do anything here as // semanticdb is instead configured in the Bazel rules. options = List( - s"-Xplugin:${info.semanticdbPluginjar}", + s"-Xplugin:${workspaceRoot.resolve(workspaceInfo.semanticdbDep)}", s"-P:semanticdb:sourceroot:${workspaceRoot}" ) ::: info.scalacOptions, classpath = @@ -131,14 +146,11 @@ object BspTaskRunner: ) case class BspTargetInfo( - scalaVersion: String, scalacOptions: List[String], classpath: List[Path], - scalaCompileJars: List[Path], srcs: List[Path], targetLabel: BazelLabel, semanticdbTargetRoot: Path, - semanticdbPluginjar: List[Path] ) object BspTargetInfo: @@ -147,15 +159,12 @@ object BspTaskRunner: } given bspTargetInfoDecoder: Decoder[BspTargetInfo] = - Decoder.forProduct8( - "scala_version", + Decoder.forProduct5( "scalac_options", "classpath", - "scala_compiler_jars", "srcs", "target_label", "semanticdb_target_root", - "semanticdb_pluginjar" )(BspTargetInfo.apply) case class BspTaskRunner( @@ -180,6 +189,16 @@ case class BspTaskRunner( ) ) + def workspaceInfo: IO[BspTaskRunner.WorkspaceInfo] = + val filePath = workspaceRoot.resolve("bazel-bin").resolve("bsp_workspace_info.json") + for + result <- runner.build( + BazelLabel.fromStringUnsafe("//:bsp_workspace_info") + ) + _ <- raiseIfNotOk(result) + deps <- FilesIO.readJson[BspTaskRunner.WorkspaceInfo](filePath) + yield deps + private def buildTargets( packageRoot: BazelLabel ): IO[List[BuildTargetIdentifier]] = @@ -203,6 +222,7 @@ case class BspTaskRunner( BuildTargetIdentifier.bazel(label) } + def buildTargets: IO[List[BuildTargetIdentifier]] = packageRoots.toList.flatTraverse(buildTargets) @@ -258,12 +278,3 @@ case class BspTaskRunner( ) } - case class BazelSources(sources: List[String], buildFiles: List[String]) - - object BazelSources: - given Decoder[BazelSources] = Decoder.instance { c => - for - sources <- c.downField("sources").as[List[String]] - buildFiles <- c.downField("buildFiles").as[List[String]] - yield BazelSources(sources, buildFiles) - } diff --git a/src/test/scala/bazeltools/bsp4bazel/runner/BspTaskRunnerTest.scala b/src/test/scala/bazeltools/bsp4bazel/runner/BspTaskRunnerTest.scala index 5a02c88..ebcae65 100644 --- a/src/test/scala/bazeltools/bsp4bazel/runner/BspTaskRunnerTest.scala +++ b/src/test/scala/bazeltools/bsp4bazel/runner/BspTaskRunnerTest.scala @@ -45,17 +45,18 @@ class BspTaskRunnerTest extends munit.CatsEffectSuite: def strToTarget(str: String): BuildTargetIdentifier = BuildTargetIdentifier.bazel(BazelLabel.fromString(str).toOption.get) - def bazelEnv(workspaceRoot: Path, packageRoots: NonEmptyList[BazelLabel]) = FunFixture[(Path, BspTaskRunner)]( - setup = { test => - val bbr = BspTaskRunner.default( - workspaceRoot, - packageRoots, - Logger.noOp - ) - (workspaceRoot, bbr) - }, - teardown = { (_, bbr) => bbr.runner.shutdown } - ) + def bazelEnv(workspaceRoot: Path, packageRoots: NonEmptyList[BazelLabel]) = + FunFixture[(Path, BspTaskRunner)]( + setup = { test => + val bbr = BspTaskRunner.default( + workspaceRoot, + packageRoots, + Logger.noOp + ) + (workspaceRoot, bbr) + }, + teardown = { (_, bbr) => bbr.runner.shutdown } + ) bazelEnv(projectRoot.resolve("examples/simple-no-errors"), packageRoots) .test("should list all project tagets") { (root, runner) => @@ -67,6 +68,25 @@ class BspTaskRunnerTest extends munit.CatsEffectSuite: ) } + bazelEnv(projectRoot.resolve("examples/simple-no-errors"), packageRoots) + .test("should return the correct metadata for a workspace") { + (root, runner) => + val wd = runner.workspaceInfo + .unsafeRunSync() + + assertEquals(wd.scalaVersion, "2.12.18") + + val fileNames = wd.scalacDeps.map(_.getFileName.toString) + assert(fileNames.exists(_.contains("scala-library-2.12.18"))) + assert(fileNames.exists(_.contains("scala-reflect-2.12.18"))) + assert(fileNames.exists(_.contains("scala-compiler-2.12.18"))) + + assert( + wd.semanticdbDep.getFileName.toString + .contains("semanticdb-scalac_2.12.18") + ) + } + bazelEnv(projectRoot.resolve("examples/simple-no-errors"), packageRoots) .test("should return the correct metadata for a given target") { (root, runner) => @@ -74,8 +94,8 @@ class BspTaskRunnerTest extends munit.CatsEffectSuite: .bspTarget(strToTarget("//src/example/foo:foo")) .unsafeRunSync() - assertEquals(bt.info.scalaVersion, "2.12.18") assertEquals(bt.info.scalacOptions, Nil) + assertEquals( bt.info.classpath.map(_.getFileName.toString).sorted, List( @@ -84,23 +104,17 @@ class BspTaskRunnerTest extends munit.CatsEffectSuite: "scala-reflect-2.12.18-stamped.jar" ) ) - assertEquals( - bt.info.scalaCompileJars.map(_.getFileName.toString).sorted, - List( - "scala-compiler-2.12.18-stamped.jar", - "scala-library-2.12.18-stamped.jar", - "scala-reflect-2.12.18-stamped.jar" - ) - ) - assertEquals( + + assertEquals( bt.info.srcs.map(_.toString).sorted, List( "src/example/foo/Bar.scala", - "src/example/foo/Foo.scala", + "src/example/foo/Foo.scala" ) ) - assertEquals(bt.info.targetLabel, BazelLabel.fromStringUnsafe("@//src/example/foo:foo")) + assertEquals( + bt.info.targetLabel, + BazelLabel.fromStringUnsafe("@//src/example/foo:foo") + ) assert(bt.info.semanticdbTargetRoot.endsWith("_semanticdb/foo")) - assert(bt.info.semanticdbPluginjar.head.endsWith("semanticdb-scalac_2.12.18-4.8.4-stamped.jar")) } -