Skip to content

Commit

Permalink
feat(plugins): Add a new plugin API with symbol processing
Browse files Browse the repository at this point in the history
Add a new plugin API with the following features:

* Plugins have a `PluginDescriptor` which holds the metadata of the
  plugin. Besides the name this also holds a list of the supported
  plugin configuration options and their types. This can be used by the
  CLI and other tools like the ORT server to show the supported options
  to the user.

* Boilerplate code like the plugin factory implementation which includes
  creating the configuration object from the plain options and secrets
  maps can be generated by a Kotlin symbol processor. To mark plugin
  classes for the symbol processor, the new `@OrtPlugin` annotation is
  introduced.

* Plugin configuration classes must use only properties with `String`,
  `Int`, `Long`, `Boolean`, `Secret`, or `List<String>` type.

To simplify the migration, the new Plugin API is created completely
separate from the old API in a new `:plugins:api` module.

Signed-off-by: Martin Nonnenmacher <martin.nonnenmacher@bosch.com>
  • Loading branch information
mnonnenmacher committed Aug 29, 2024
1 parent f787654 commit 71983f1
Show file tree
Hide file tree
Showing 12 changed files with 989 additions and 0 deletions.
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ gitSemverPlugin = "0.12.10"
graalVmNativeImagePlugin = "0.10.2"
ideaExtPlugin = "1.1.8"
kotlinPlugin = "2.0.20"
ksp = "2.0.20-1.0.24"
mavenPublishPlugin = "0.29.0"
versionsPlugin = "0.51.0"

Expand All @@ -33,6 +34,7 @@ jslt = "0.1.14"
jsonSchemaValidator = "1.5.1"
kaml = "0.61.0"
kotest = "5.9.1"
kotlinPoet = "1.18.1"
kotlinxCoroutines = "1.8.1"
kotlinxHtml = "0.11.0"
kotlinxSerialization = "1.7.2"
Expand Down Expand Up @@ -69,6 +71,7 @@ download = { id = "de.undercouch.download", version.ref = "downloadPlugin" }
gitSemver = { id = "com.github.jmongard.git-semver-plugin", version.ref = "gitSemverPlugin" }
ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExtPlugin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinPlugin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" }

[libraries]
Expand Down Expand Up @@ -124,6 +127,8 @@ kotest-framework-api = { module = "io.kotest:kotest-framework-api", version.ref
kotest-framework-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest" }
kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoet"}
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinPoet"}
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinxHtml" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
Expand All @@ -133,6 +138,7 @@ kotlinx-serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization
kotlinx-serialization-yaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml" }
ks3-jdk = { module = "io.ks3:ks3-jdk", version.ref = "ks3" }
ks3-standard = { module = "io.ks3:ks3-standard", version.ref = "ks3" }
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp"}
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4jApi" }
Expand Down
149 changes: 149 additions & 0 deletions plugins/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# ORT Plugin API

The ORT Plugin API defines the interfaces that ORT plugin extension points must use and that ORT plugins must implement.

## Plugin Extension Points

Plugin extension points must define the base class for the plugin and an interface for a factory that creates instances of the plugin.
For example, the extension point for advisor plugins is defined in the `AdviceProvider` interface and the `AdviceProviderFactory` interface.

### Plugin Base Class

The plugin base class defines the functions and properties that plugins must implement.
The base class can be either an interface or an abstract class and must extend the `Plugin` interface.
If it is an abstract class, it must not take any constructor arguments, as this would make it impossible to define a generic factory function for all plugins.

For example, the `AdviceProvider` interface defines one function and one property that all advisor plugins must implement:

```kotlin
interface AdviceProvider : Plugin {
val details: AdvisorDetails
suspend fun retrievePackageFindings(packages: Set<Package>): Map<Package, AdvisorResult>
}
```

In addition, the `Plugin` interface defines a `descriptor` property that contains metadata about the plugin:

```kotlin
interface Plugin {
val descriptor: PluginDescriptor
}
```

### Plugin Factory Interface

The plugin factory interface is a markup interface that extends the `PluginFactory` interface and defines the plugin base class as a type parameter.

For example, the `AdviceProviderFactory` interface defines that the base class for advisor plugins is `AdviceProvider`:

```kotlin
interface AdviceProviderFactory : PluginFactory<AdviceProvider>
```

The `create` function defined by the `PluginFactory` interface takes has a `PluginConfig` parameter that contains the configuration for the plugin.
The `PluginFactory` provides a generic `getAll<T>()` function that returns all available plugin factories of the given type.
It uses the service loader mechanism to find the available plugin factories.

## Plugin Implementations

Plugin implementations consist of a class that implements the plugin base class, a factory class that implements the factory interface, and a service loader configuration file.
If the plugin has configuration options, it must implement an additional data class as a holder for the configuration.

To reduce the amount of boilerplate code, the plugin API provides a compiler plugin that can generate the factory class and the service loader file.
The compiler plugin uses the [Kotlin Symbol Processing (KSP) API](https://kotlinlang.org/docs/ksp-overview.html).
With this, the plugin implementation only needs to implement the plugin class and the configuration data class.

### Plugin Class

To be able to use the compiler plugin, the plugin class must follow certain conventions:

* It must be annotated with the `@OrtPlugin` annotation which takes some metadata and the factory interface as arguments.
* It must have a single constructor that takes one or two arguments:
The first one must override the `descriptor` property of the `Plugin` interface.
Optionally, the second one must be called `config` and must be of the type of the configuration data class.

For example, an advisor plugin implementation could look like this:

```kotlin
@OrtPlugin(
name = "Example Advisor",
description = "An example advisor plugin.",
factory = AdviceProviderFactory::class
)
class ExampleAdvisor(override val descriptor: PluginDescriptor, val config: ExampleConfiguration) : AdviceProvider {
...
}
```

### Plugin Configuration Class

The configuration class must be a data class with a single constructor that takes all configuration options as arguments.
To be able to use the compiler plugin, the configuration class must follow certain conventions:

* All constructor arguments must be `val`s.
* Constructor arguments must have one of the following types: `Boolean`, `Int`, `Long`, `Secret`, `String`, `List<String>`.
* Constructor arguments must not have a default value.
Instead, the default value can be set by adding the `@OrtPluginOption` annotation to the property.
This is required for code generation because KSP does not provide any details about default values of constructor arguments.
Also, to be able to handle default values in the compiler plugin, they must be compile time constants which also applies to annotation arguments.
* Constructor arguments can be nullable if they are optional.
* If a constructor argument is not nullable and has no default value, the argument is required and the generated factory will throw an exception if it cannot be found in the `PluginConfig`.
* The compiler plugin will use the KDoc of a constructor argument as the description of the option when generating the `PluginDescriptor`.

The generated factory class will take option values from the `PluginConfig.options` map and use them to create an instance of the configuration class.
The only exception are `Secret` properties which are taken from the `PluginConfig.secrets` map.

For example, an advisor plugin configuration could look like this:

```kotlin
data class ExampleConfiguration(
/** The REST API server URL. */
@OrtPluginOption(defaultValue = "https://example.com")
val serverUrl: String,

/** The timeout in seconds for REST API requests. */
val timeout: Int,

/** The API token to use for authentication. */
val token: Secret?
)
```

Here, the `serverUrl` property has a default value, the `timeout` property is required, and the `token` property is optional.

### Gradle Configuration

A Gradle module that contains an ORT plugin implementation must apply the `com.google.devtools.ksp` Gradle plugin:

```kotlin
plugins {
id("com.google.devtools.ksp:[version]")
}
```

Or in the ORT codebase:

```kotlin
plugins {
alias(libs.plugins.ksp)
}
```

It also must add the ORT plugin API and the API of the implemented extension point to the KSP configuration.
For example, an advisor plugin implementation would add the following dependencies:

```kotlin
dependencies {
ksp("org.ossreviewtoolkit:advisor:[version]")
ksp("org.ossreviewtoolkit:plugins-api:[version]")
}
```

Or in the ORT codebase:

```kotlin
dependencies {
ksp(projects.advisor)
ksp(projects.plugins.api)
}
```
31 changes: 31 additions & 0 deletions plugins/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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
*/

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

dependencies {
api(projects.utils.commonUtils)

implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
implementation(libs.ksp)
}
32 changes: 32 additions & 0 deletions plugins/api/src/main/kotlin/OrtPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.plugins.api

import kotlin.reflect.KClass

/**
* An annotation to mark a class as an ORT plugin.
*/
@Target(AnnotationTarget.CLASS)
annotation class OrtPlugin(
val name: String,
val description: String,
val factory: KClass<*>
)
32 changes: 32 additions & 0 deletions plugins/api/src/main/kotlin/OrtPluginOption.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.plugins.api

/**
* An annotation to mark a property as an option for an ORT plugin. The annotation is only required to set a default
* value for the option.
*/
@Target(AnnotationTarget.PROPERTY)
annotation class OrtPluginOption(
/**
* The default value of the option.
*/
val defaultValue: String
)
47 changes: 47 additions & 0 deletions plugins/api/src/main/kotlin/PluginConfig.kt
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
*/

package org.ossreviewtoolkit.plugins.api

import com.fasterxml.jackson.annotation.JsonIgnore

import org.ossreviewtoolkit.utils.common.Options

/**
* The configuration of a plugin, used as input for [PluginFactory.create].
*/
data class PluginConfig(
/**
* The configuration options of the plugin.
*/
val options: Options = emptyMap(),

/**
* The configuration secrets of the plugin.
*
* This property is not serialized to ensure that secrets do not appear in serialized output.
*/
@JsonIgnore
val secrets: Options = emptyMap()
) {
/**
* Return a string representation that does not contain the [secrets].
*/
override fun toString() = "${this::class.simpleName}(options=$options, secrets=[***])"
}
Loading

0 comments on commit 71983f1

Please sign in to comment.