Skip to content

Commit

Permalink
feature: Add dependencyModules request support (#536)
Browse files Browse the repository at this point in the history
* feature: Add dependencyModules request support

This would be super useful to use as an alternative to libraries request, which is not standard BSP request.

* chore: Add unit tests for dependency modules

* bugfix: Make dependency modules work for bazelmod
  • Loading branch information
tgodzik committed Mar 27, 2024
1 parent b117aec commit e62f321
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 2 deletions.
Expand Up @@ -7,6 +7,11 @@ import ch.epfl.scala.bsp4j.CompileProvider
import ch.epfl.scala.bsp4j.CppOptionsItem
import ch.epfl.scala.bsp4j.CppOptionsParams
import ch.epfl.scala.bsp4j.CppOptionsResult
import ch.epfl.scala.bsp4j.DependencyModule
import ch.epfl.scala.bsp4j.DependencyModuleDataKind
import ch.epfl.scala.bsp4j.DependencyModulesItem
import ch.epfl.scala.bsp4j.DependencyModulesParams
import ch.epfl.scala.bsp4j.DependencyModulesResult
import ch.epfl.scala.bsp4j.DependencySourcesItem
import ch.epfl.scala.bsp4j.DependencySourcesParams
import ch.epfl.scala.bsp4j.DependencySourcesResult
Expand Down Expand Up @@ -460,6 +465,26 @@ class BspProjectMapper(
return ScalaMainClassesResult(items)
}

fun buildDependencyModules(project: Project, params: DependencyModulesParams): DependencyModulesResult {
val targetSet = params.targets.toSet()
val dependencyModulesItems = project.modules.filter { targetSet.contains(BuildTargetIdentifier(it.label.value)) }.map { module ->
val buildTargetId = BuildTargetIdentifier(module.label.value)
val moduleItems = DependencyMapper.allModuleDependencies(project, module).flatMap { libraryDep ->
if (libraryDep.outputs.isNotEmpty()) {
val mavenDependencyModule = DependencyMapper.extractMavenDependencyInfo(libraryDep)
val dependencyModule = DependencyModule(libraryDep.label, mavenDependencyModule?.version ?: "")
if (mavenDependencyModule != null) {
dependencyModule.data = mavenDependencyModule
dependencyModule.dataKind = DependencyModuleDataKind.MAVEN
}
listOf(dependencyModule)
} else emptyList()
}
DependencyModulesItem(buildTargetId, moduleItems)
}
return DependencyModulesResult(dependencyModulesItems)
}

fun rustWorkspace(
project: Project,
params: RustWorkspaceParams
Expand Down
@@ -0,0 +1,59 @@
package org.jetbrains.bsp.bazel.server.sync

import ch.epfl.scala.bsp4j.MavenDependencyModule
import ch.epfl.scala.bsp4j.MavenDependencyModuleArtifact
import org.jetbrains.bsp.bazel.server.sync.model.Library
import org.jetbrains.bsp.bazel.server.sync.model.Module
import org.jetbrains.bsp.bazel.server.sync.model.Project
import java.util.*

object DependencyMapper {

fun extractMavenDependencyInfo(lib: Library): MavenDependencyModule? {
if (lib.outputs.isEmpty()) return null
val jars = lib.outputs.map { uri -> uri.toString() }.map {
MavenDependencyModuleArtifact(it)
}
val sourceJars = lib.sources.map { uri -> uri.toString() }.map {
val artifact = MavenDependencyModuleArtifact(it)
artifact.classifier = "sources"
artifact
}
/* For example:
* @@rules_jvm_external~override~maven~maven//:org_apache_commons_commons_lang3
* @maven//:org_scala_lang_scala_library
**/
val orgStart = lib.label.split("//:").lastOrNull()?.split('_')?.firstOrNull() ?: "org"
// Matches the Maven group (organization), artifact, and version in the Bazel dependency
// string such as .../execroot/monorepo/bazel-out/k8-fastbuild/bin/external/maven/com/google/guava/guava/31.1-jre/processed_guava-31.1-jre.jar
// bazel-out/k8-fastbuild/bin/external/rules_jvm_external~~maven~name/v1/https/repo1.maven.org/maven2/com/google/auto/service/auto-service-annotations/1.1.1/header_auto-service-annotations-1.1.1.jar
val regexPattern = """.*/($orgStart/.+)/([^/]+)/([^/]+)/[^/]+.jar""".toRegex()
val dependencyPath = lib.outputs.firstOrNull()?.toString()
// Find matches in the dependency path
if (dependencyPath != null) {
val matchResult = regexPattern.find(dependencyPath)
// If a match is found, group values are extracted; otherwise, null is returned
return matchResult?.let {
val (organization, artifact, version) = it.destructured
MavenDependencyModule(organization.replace("/", "."), artifact, version, jars + sourceJars)
}
} else {
return null
}
}


fun allModuleDependencies(project: Project, module: Module): HashSet<Library> {
val toResolve = LinkedList<String>()
toResolve.addAll(module.directDependencies.map { it.value })
val accumulator = HashSet<Library>()
while (toResolve.isNotEmpty()) {
val lib = project.libraries[toResolve.pop()]
if (lib != null && !accumulator.contains(lib)) {
accumulator.add(lib)
toResolve.addAll(lib.dependencies)
}
}
return accumulator
}
}
Expand Up @@ -167,8 +167,8 @@ class ProjectSyncService(private val bspMapper: BspProjectMapper, private val pr
cancelChecker: CancelChecker,
params: DependencyModulesParams
): DependencyModulesResult {
// TODO https://youtrack.jetbrains.com/issue/BAZEL-616
return DependencyModulesResult(emptyList())
val project = projectProvider.get(cancelChecker)
return bspMapper.buildDependencyModules(project, params)
}

fun rustWorkspace(
Expand Down
Expand Up @@ -18,3 +18,12 @@ kt_test(
"//server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync",
],
)

kt_test(
name = "DependencyMapperTest",
size = "small",
src = "DependencyMapperTest.kt",
deps = [
"//server/src/main/kotlin/org/jetbrains/bsp/bazel/server/sync",
],
)
@@ -0,0 +1,126 @@
package org.jetbrains.bsp.bazel.server.sync

import ch.epfl.scala.bsp4j.MavenDependencyModule
import ch.epfl.scala.bsp4j.MavenDependencyModuleArtifact
import io.kotest.matchers.shouldBe
import org.jetbrains.bsp.bazel.bazelrunner.BazelRelease
import org.jetbrains.bsp.bazel.server.sync.model.Label
import org.jetbrains.bsp.bazel.server.sync.model.Library
import org.jetbrains.bsp.bazel.server.sync.model.Module
import org.jetbrains.bsp.bazel.server.sync.model.Project
import org.jetbrains.bsp.bazel.server.sync.model.SourceSet
import org.junit.jupiter.api.Test
import java.net.URI
import java.nio.file.Paths

class DependencyMapperTest {

private val cacheLocation = "file:///home/user/.cache/bazel/_bazel_user/ae7b7b315151086e31e3b97f9ddba009/execroot/monorepo/bazel-out/k8-fastbuild-ST-4a519fd6d3e4"

@Test
fun `should translate dependency`() {
val jarUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/processed_scala-library-2.13.11.jar")
val jarSourcesUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/scala-library-2.13.11-sources.jar")
val lib1 = Library(
"@maven//:org_scala_lang_scala_library",
setOf(jarUri),
setOf(jarSourcesUri),
emptyList()
)
val expectedMavenArtifact = MavenDependencyModuleArtifact(jarUri.toString())
val expectedMavenSourcesArtifact = MavenDependencyModuleArtifact(jarSourcesUri.toString())
expectedMavenSourcesArtifact.classifier = "sources"
val expectedDependency = MavenDependencyModule("org.scala-lang", "scala-library", "2.13.11", listOf(
expectedMavenArtifact,
expectedMavenSourcesArtifact
))
val dependency = DependencyMapper.extractMavenDependencyInfo(lib1)

dependency shouldBe expectedDependency
}

@Test
fun `should bazelmod translate dependency`() {
val jarUri = URI.create("$cacheLocation/bin/external/rules_jvm_external~~maven~name/v1/https/repo1.maven.org/maven2/com/google/auto/service/auto-service-annotations/1.1.1/header_auto-service-annotations-1.1.1.jar")
val jarSourcesUri = URI.create("$cacheLocation/bin/external/rules_jvm_external~~maven~name/v1/https/repo1.maven.org/maven2/com/google/auto/service/auto-service-annotations/1.1.1/header_auto-service-annotations-1.1.1-sources.jar")
val lib1 = Library(
"@@rules_jvm_external~override~maven~maven//:com_google_auto_service_auto_service_annotations",
setOf(jarUri),
setOf(jarSourcesUri),
emptyList()
)
val expectedMavenArtifact = MavenDependencyModuleArtifact(jarUri.toString())
val expectedMavenSourcesArtifact = MavenDependencyModuleArtifact(jarSourcesUri.toString())
expectedMavenSourcesArtifact.classifier = "sources"
val expectedDependency = MavenDependencyModule("com.google.auto.service", "auto-service-annotations", "1.1.1", listOf(
expectedMavenArtifact,
expectedMavenSourcesArtifact
))
val dependency = DependencyMapper.extractMavenDependencyInfo(lib1)

dependency shouldBe expectedDependency
}

@Test
fun `should not translate non maven dependency`() {
val lib1 = Library(
"@//projects/v1:scheduler",
emptySet(),
emptySet(),
emptyList()
)
val dependency = DependencyMapper.extractMavenDependencyInfo(lib1)

dependency shouldBe null
}

@Test
fun `should gather deps transitively`() {
val jarUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/processed_scala-library-2.13.11.jar")
val jarSourcesUri = URI.create("$cacheLocation/bin/external/maven/org/scala-lang/scala-library/2.13.11/scala-library-2.13.11-sources.jar")
val lib1 = Library(
"@maven//:org_scala_lang_scala_library",
setOf(jarUri),
setOf(jarSourcesUri),
emptyList()
)
val lib2 = Library(
"@maven//:org_scala_lang_scala_library2",
emptySet(),
emptySet(),
listOf(lib1.label)
)
val lib3 = Library(
"@maven//:org_scala_lang_scala_library3",
emptySet(),
emptySet(),
listOf(lib1.label, lib2.label)
)
val lib4 = Library(
"@maven//:org_scala_lang_scala_library4",
emptySet(),
emptySet(),
listOf(lib3.label, lib2.label)
)
val libraries = mapOf(lib1.label to lib1, lib2.label to lib2, lib3.label to lib3, lib4.label to lib4)
val currentUri = Paths.get(".").toUri()
val project = Project(currentUri, emptyList(), emptyMap(), libraries, emptyList(), BazelRelease(6))
val module = Module(
Label(""),
true,
listOf(Label(lib4.label)),
emptySet(),
emptySet(),
currentUri,
SourceSet(emptySet(), emptySet()),
emptySet(),
emptySet(),
emptySet(),
null,
emptyMap()
)
val foundLibraries = DependencyMapper.allModuleDependencies(project, module)

foundLibraries shouldBe setOf(lib1, lib2, lib3, lib4)
}
}

0 comments on commit e62f321

Please sign in to comment.