Skip to content

Commit

Permalink
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 e8b88df
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 0 deletions.
47 changes: 47 additions & 0 deletions clients/bazel-module-registry/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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(projects.model)
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,52 @@
/*
* 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

import org.ossreviewtoolkit.model.HashAlgorithm

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

"getModuleMetadata" should {
"include a homepage URL, non-empty VCS info and version 0.5.0 for the 'glog' module" {
val metadata = service.getModuleMetadata("glog")

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

"getModuleSource" should {
"return non-empty remote artifact info" {
val sourceInfo = service.getModuleSourceInfo("glog", "0.5.0")

sourceInfo.remoteArtifact().url shouldBe "${repoUrl}/archive/refs/tags/v0.5.0.tar.gz"
sourceInfo.remoteArtifact().hash.algorithm shouldBe HashAlgorithm.SHA256
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright (C) 2020 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 java.net.URI
import java.util.Base64

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
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

import org.ossreviewtoolkit.model.Hash
import org.ossreviewtoolkit.model.HashAlgorithm
import org.ossreviewtoolkit.model.RemoteArtifact
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType

@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())
}

/**
* 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 BazelModuleRegistryService {
companion object {
/**
* 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.
*/
const val DEFAULT_URL = "https://bcr.bazel.build"

/**
* 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 = DEFAULT_URL,
client: OkHttpClient? = null
): BazelModuleRegistryService {
val bmrClient = client ?: OkHttpClient()

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

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

@Serializable
data class ModuleMetadata(
@Serializable(URISerializer::class) val homepage: URI?,
val maintainers: List<ModuleMaintainer>? = 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>,
) {
fun vcsInfo(): VcsInfo {
if (repository.isNullOrEmpty()) {
return VcsInfo.EMPTY
}

val repo = repository[0]
// From looking at all current values of this field on BCR, it looks like only the special value "github:"
// exists. Otherwise it's just the repo URL.
val url = if (repo.startsWith("github:")) {
val path = repo.substringAfter("github:")
"https://github.com/${path}"
} else {
repo
}

return VcsInfo(
type = VcsType.GIT,
url = url,
revision = "",
path = "",
)
}
}

@Serializable
data class ModuleMaintainer(
val email: String? = null,
val name: String? = null
)

@Serializable
data class ModuleSource(
val integrity: String,
val patchStrip: Int? = null,
val patches: Map<String, String>? = null,
val stripPrefix: String? = null,
@Serializable(URISerializer::class) val url: URI,
) {
fun remoteArtifact() = RemoteArtifact(url = url.toString(), hash = hash())

private fun hash(): Hash {
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

val (algo, b64digest) = integrity.split("-", limit = 2)
val digest = Base64.getDecoder().decode(b64digest).toHex()

return Hash(
value = digest,
algorithm = HashAlgorithm.fromString(algo),
)
}
}

/**
* 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): ModuleSource
}
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 e8b88df

Please sign in to comment.