The ORT Plugin API defines the interfaces that ORT plugin extension points must use and that ORT plugins must implement.
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.
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:
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:
interface Plugin {
val descriptor: PluginDescriptor
}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:
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 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, ORT 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. With this, the plugin implementation only needs to implement the plugin class and the configuration data class.
To be able to use the compiler plugin, the plugin class must follow certain conventions:
- It must be annotated with the
@OrtPluginannotation 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
descriptorproperty of thePlugininterface. Optionally, the second one must be calledconfigand must be of the type of the configuration data class.
For example, an advisor plugin implementation could look like this:
@OrtPlugin(
name = "Example Advisor",
description = "An example advisor plugin.",
factory = AdviceProviderFactory::class
)
class ExampleAdvisor(override val descriptor: PluginDescriptor, val config: ExampleConfiguration) : AdviceProvider {
...
}The KDoc of the plugin class is used as the description of the plugin when generating the PluginDescriptor.
Therefore, it should be written with the intention to be read by users of the plugin.
For example, for package managers, it should document requirements and limitations of the plugin, helping users to successfully analyze their projects.
As the description is also used for the website, certain limitations apply:
- It should not use reference-style links (like
[some website][1]). - It should not use footnotes (like
[^1]). - It should only use headings of level 3 or higher (like
### Heading). - KDoc symbol references (like
[SomeClass]) are replaced with inline-code blocks.
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
vals. - Constructor arguments must have one of the following types:
Boolean,Int,Long,Secret,String,List<String>. Also supported are enum types and lists of enum types. - Constructor arguments must not have a default value.
Instead, the default value can be set by adding the
@OrtPluginOptionannotation 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 contains a factory function with a PluginConfig argument.
The option values are taken from the PluginConfig.options map and are used to create an instance of the configuration class.
The only exception are Secret properties which are taken from the PluginConfig.secrets map.
In addition, the generated factory class contains a static factory function which takes the properties of the config class as arguments.
This is convenient when creating plugin instances in test code, because the arguments of this function are type-safe and automatically initialized with the default values defined in the @OrtPluginOption annotation.
For example, an advisor plugin configuration could look like this:
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. */
@OrtPluginOption(aliases = ["apiToken", "authToken"])
val token: Secret?
)Here, the serverUrl property has a default value, the timeout property is required, and the token property is optional.
The token property also has two aliases defined which can be used as alternative names for the option in the plugin configuration.
This is useful to provide backward compatibility when renaming options.
The generated ExampleAdvisorFactory class will contain two factory functions:
class ExampleAdvisorProviderFactory : AdviceProviderFactory {
override fun create(config: PluginConfig): ExampleAdvisor {
val configObject = ExampleConfiguration(
serverUrl = parseStringOption("serverUrl", config),
timeout = parseIntegerOption("timeout", config),
token = parseNullableSecretOption("token", config)
)
return ExampleAdvisor(descriptor, configObject)
}
companion object {
fun create(
serverUrl: String = "https://example.com",
timeout: Int,
token: Secret? = null
): ExampleAdvisor {
val configObject = ExampleConfiguration(
serverUrl = serverUrl,
timeout = timeout,
token = token
)
return ExampleAdvisor(descriptor, configObject)
}
}
}If there is only a predefined set of values that a configuration option can take, it is recommended to use an enum type for the corresponding constructor argument.
The compiler plugin will automatically handle the conversion from a string value to the enum constant.
For example, the following configuration class uses an enum type for the outputFormat option:
enum class OutputFormat {
JSON,
XML,
YAML
}
data class ExampleConfiguration(
/** The output format to use. */
@OrtPluginOption(defaultValue = "JSON")
val outputFormat: OutputFormat
)It is possible to use lists of enum types as well:
data class ExampleConfiguration(
/** The output formats to use. */
@OrtPluginOption(defaultValue = "JSON,XML")
val outputFormats: List<OutputFormat>
)Values are parsed case-insensitively, so the values json, JSON, and Json are all valid and will be converted to the OutputFormat.JSON constant.
If a desired value is not a valid JVM identifier (e.g. it contains hyphens), the @OrtPluginEnumEntry annotation can be used to define an alternative name for the enum constant:
enum class Version {
@OrtPluginEnumEntry(alternativeName = "1.0")
VERSION_1_0
}In this case, only the value 1.0 will be accepted in the plugin configuration to refer to the Version.VERSION_1_0 constant.
It is also possible to define a list of aliases for an enum constant, for example, to support backward compatibility when renaming enum entries:
enum class LogLevel {
@OrtPluginEnumEntry(alternativeNames = ["WARN", "W"])
WARNING
}Here, the values WARNING, WARN and W will all be mapped to the LogLevel.WARNING constant.
A Gradle module that contains an ORT plugin implementation must apply the com.google.devtools.ksp Gradle plugin and add dependencies to the ORT compiler plugin and the API of the implemented extension point to the KSP configuration:
plugins {
id("com.google.devtools.ksp:[version]")
}
dependencies {
ksp("org.ossreviewtoolkit:advisor:[version]")
ksp("org.ossreviewtoolkit.plugins:compiler:[version]")
}In the ORT codebase, the ort-plugin-conventions should be applied so that only a dependency on the extension point API is required:
plugins {
id("ort-plugin-conventions")
}
dependencies {
ksp(projects.advisor)
}