Skip to content

Commit

Permalink
feat(clients): Add Bazel module registry service
Browse files Browse the repository at this point in the history
A Bazel module registry, such as https://bcr.bazel.build,
is used to store metadata of dependencies managed by bzlmod.

Signed-off-by: Haiko Schol <[email protected]>
  • Loading branch information
haikoschol committed Mar 13, 2024
1 parent c81a79a commit 67315ee
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 0 deletions.
46 changes: 46 additions & 0 deletions clients/bazel-module-registry/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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
*
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
// Apply precompiled plugins.
id("ort-library-conventions")

// Apply third-party plugins.
alias(libs.plugins.kotlinSerialization)
}

dependencies {
api(libs.okhttp)
api(libs.retrofit)

implementation(libs.bundles.kotlinxSerialization)
implementation(libs.retrofit.converter.kotlinxSerialization)
}

tasks.withType<KotlinCompile>().configureEach {
val customCompilerArgs = listOf(
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
)

compilerOptions {
freeCompilerArgs.addAll(customCompilerArgs)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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
*
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.clients.bazelmoduleregistry

import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.comparables.shouldBeGreaterThan
import io.kotest.matchers.shouldBe

class BazelModuleRegistryClientTest : WordSpec({
val service = BazelModuleRegistryClient.create()
val repoUrl = "https://github.com/google/glog"

"getModuleMetadata" should {
"include a homepage URL and version 0.5.0 for the 'glog' module" {
val metadata = service.getModuleMetadata("glog")

metadata.homepage.toString() shouldBe repoUrl
metadata.versions.size shouldBeGreaterThan 1
metadata.versions shouldContain "0.5.0"
}
}

"getModuleSource" should {
"include URL and hash data" {
val sourceInfo = service.getModuleSourceInfo("glog", "0.5.0")

sourceInfo.url.toString() shouldBe "$repoUrl/archive/refs/tags/v0.5.0.tar.gz"
sourceInfo.integrity shouldBe "sha256-7t5x8oNxvzmqabRd4jsynTchQBbiBVJps7Xnz9QLWfU="
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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
*
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.clients.bazelmoduleregistry

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient

import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path

/**
* The JSON (de-)serialization object used by this service.
*/
private val JSON = Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
}

/**
* The service uses the Bazel Central Registry by default.
*/
private const val DEFAULT_URL = "https://bcr.bazel.build"

/**
* Interface for a Bazel Module Registry, based on the directory structure of https://bcr.bazel.build
* and the git repository it is based on (https://github.com/bazelbuild/bazel-central-registry/).
*/
interface BazelModuleRegistryClient {
companion object {
/**
* Create a Bazel Module Registry service instance for communicating with a server running at the given [url],
* defaulting to the Bazel Central Registry, optionally with a pre-built OkHttp [client].
*/
fun create(
url: String? = null,
client: OkHttpClient? = null
): BazelModuleRegistryClient {
val bmrClient = client ?: OkHttpClient()

val contentType = "application/json".toMediaType()
val retrofit = Retrofit.Builder()
.client(bmrClient)
.baseUrl(url ?: DEFAULT_URL)
.addConverterFactory(JSON.asConverterFactory(contentType))
.build()

return retrofit.create(BazelModuleRegistryClient::class.java)
}
}

/**
* Retrieve the metadata for a module.
* E.g. https://bcr.bazel.build/modules/glog/metadata.json.
*/
@GET("modules/{name}/metadata.json")
suspend fun getModuleMetadata(@Path("name") name: String): ModuleMetadata

/**
* Retrieve information about the source code for a specific version of a module.
* E.g. https://bcr.bazel.build/modules/glog/0.5.0/source.json.
*/
@GET("modules/{name}/{version}/source.json")
suspend fun getModuleSourceInfo(@Path("name") name: String, @Path("version") version: String): ModuleSourceInfo
}
65 changes: 65 additions & 0 deletions clients/bazel-module-registry/src/main/kotlin/Model.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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
*
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.clients.bazelmoduleregistry

import java.net.URI

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializer(URI::class)
object URISerializer : KSerializer<URI> {
override fun serialize(encoder: Encoder, value: URI) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder) = URI(decoder.decodeString())
}

/**
* E.g. https://bcr.bazel.build/modules/glog/metadata.json.
*/
@Serializable
data class ModuleMetadata(
@Serializable(URISerializer::class) val homepage: URI?,
val maintainers: List<Maintainer>? = null,
val repository: List<String>? = null,
val versions: List<String>,
// The key in the map is the version, the value the reason for yanking it.
val yankedVersions: Map<String, String>,
) {
@Serializable
data class Maintainer(
val email: String? = null,
val name: String? = null
)
}

/**
* E.g. https://bcr.bazel.build/modules/glog/0.5.0/source.json.
*/
@Serializable
data class ModuleSourceInfo(
val integrity: String,
val patchStrip: Int? = null,
val patches: Map<String, String>? = null,
val stripPrefix: String? = null,
@Serializable(URISerializer::class) val url: URI,
)
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ rootProject.name = "oss-review-toolkit"
include(":advisor")
include(":analyzer")
include(":cli")
include(":clients:bazel-module-registry")
include(":clients:clearly-defined")
include(":clients:fossid-webapp")
include(":clients:github-graphql")
Expand All @@ -48,6 +49,7 @@ include(":utils:scripting")
include(":utils:spdx")
include(":utils:test")

project(":clients:bazel-module-registry").name = "bazel-module-registry-client"
project(":clients:clearly-defined").name = "clearly-defined-client"
project(":clients:fossid-webapp").name = "fossid-webapp-client"
project(":clients:github-graphql").name = "github-graphql-client"
Expand Down

0 comments on commit 67315ee

Please sign in to comment.