Skip to content

Commit

Permalink
feat(plugin): new GraalVm metadata Gradle task (#1743)
Browse files Browse the repository at this point in the history
Create new Gradle task that automatically generates GraalVm reachability metadata for GraphQL Kotlin servers.

Following metadata files are generated:
* `native-image.properties`
* `reflect-config.json`
* `resource-config.json`

Usage:

```
plugins {
  kotlin("jvm") version "1.7.21"
  application
  id("org.graalvm.buildtools.native") version "0.9.20"
  id("com.expediagroup.graphql") version $graphQLKotlinVersion
}

// other build configuration goes here

graphql {
  graalVm {
    packages = listOf("com.example")
  }
}
```
  • Loading branch information
dariuszkuc authored Apr 11, 2023
1 parent ea6d503 commit f4ba9b4
Show file tree
Hide file tree
Showing 21 changed files with 631 additions and 107 deletions.
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ classgraph = "4.8.149"
dataloader = "3.2.0"
federation = "2.2.0"
graphql-java = "19.2"
graalvm = "0.9.20"
jackson = "2.14.1"
kotlin = "1.7.21"
kotlinx-benchmark = "0.4.4"
Expand Down Expand Up @@ -108,6 +109,7 @@ wiremock-standalone = { group = "com.github.tomakehurst", name = "wiremock-jre8-
# build src plugin libraries
detekt-plugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" }
dokka-plugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" }
graalvm-plugin = { group = "org.graalvm.buildtools.native", name = "org.graalvm.buildtools.native.gradle.plugin", version.ref = "graalvm" }
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ktlint-plugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint-plugin" }

Expand Down Expand Up @@ -135,5 +137,6 @@ plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "plugin-publi
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

# test projects
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "android-plugin" }
graalvm-native = { id = "org.graalvm.buildtools.native", version.ref = "graalvm" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
4 changes: 3 additions & 1 deletion plugins/graphql-kotlin-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ plugins {

dependencies {
implementation(libs.kotlin.gradle.api)
compileOnly(libs.android.plugin)

compileOnly(libs.android.plugin)
compileOnly(libs.graalvm.plugin)
compileOnly(projects.graphqlKotlinClientGenerator)
compileOnly(projects.graphqlKotlinSdlGenerator)
compileOnly(projects.graphqlKotlinGraalvmMetadataGenerator)

testImplementation(libs.wiremock.jre8)
testImplementation(libs.junit.params)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2023 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,26 +20,33 @@ import com.expediagroup.graphql.plugin.gradle.tasks.DOWNLOAD_SDL_TASK_NAME
import com.expediagroup.graphql.plugin.gradle.tasks.GENERATE_CLIENT_TASK_NAME
import com.expediagroup.graphql.plugin.gradle.tasks.GENERATE_SDL_TASK_NAME
import com.expediagroup.graphql.plugin.gradle.tasks.GENERATE_TEST_CLIENT_TASK_NAME
import com.expediagroup.graphql.plugin.gradle.tasks.GRAALVM_METADATA_TASK_NAME
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLDownloadSDLTask
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLGenerateClientTask
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLGenerateSDLTask
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLGenerateTestClientTask
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLGraalVmMetadataTask
import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLIntrospectSchemaTask
import com.expediagroup.graphql.plugin.gradle.tasks.INTROSPECT_SCHEMA_TASK_NAME
import org.graalvm.buildtools.gradle.NativeImagePlugin.NATIVE_COMPILE_TASK_NAME
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.plugins.ApplicationPlugin
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.jvm.tasks.Jar
import java.io.File

private const val PLUGIN_EXTENSION_NAME = "graphql"
private const val GENERATE_CLIENT_CONFIGURATION = "graphqlClient"
private const val GENERATE_SDL_CONFIGURATION = "graphqlSDL"
private const val GRAALVM_METADATA_CONFIGURATION = "graphqlGraalVM"
private const val GRAALVM_PLUGIN_NAME = "org.graalvm.buildtools.native"

/**
* GraphQL Kotlin Gradle Plugin
*/
@Suppress("UnstableApiUsage")
class GraphQLGradlePlugin : Plugin<Project> {

override fun apply(project: Project) {
Expand All @@ -49,7 +56,7 @@ class GraphQLGradlePlugin : Plugin<Project> {
val extension = project.extensions.create(PLUGIN_EXTENSION_NAME, GraphQLPluginExtension::class.java)
project.afterEvaluate {
processExtensionConfiguration(project, extension)
configureTaskClasspaths(project)
configureTasks(project)
}
}

Expand All @@ -69,6 +76,14 @@ class GraphQLGradlePlugin : Plugin<Project> {

configuration.dependencies.add(project.dependencies.create("com.expediagroup:graphql-kotlin-sdl-generator:$DEFAULT_PLUGIN_VERSION"))
}

project.configurations.create(GRAALVM_METADATA_CONFIGURATION) { configuration ->
configuration.isVisible = true
configuration.isTransitive = true
configuration.description = "Configuration for generating GraalVM reflect metadata"

configuration.dependencies.add(project.dependencies.create("com.expediagroup:graphql-kotlin-graalvm-metadata-generator:$DEFAULT_PLUGIN_VERSION"))
}
}

private fun registerTasks(project: Project) {
Expand All @@ -77,6 +92,15 @@ class GraphQLGradlePlugin : Plugin<Project> {
project.tasks.register(GENERATE_TEST_CLIENT_TASK_NAME, GraphQLGenerateTestClientTask::class.java)
project.tasks.register(GENERATE_SDL_TASK_NAME, GraphQLGenerateSDLTask::class.java)
project.tasks.register(INTROSPECT_SCHEMA_TASK_NAME, GraphQLIntrospectSchemaTask::class.java)
project.tasks.register(GRAALVM_METADATA_TASK_NAME, GraphQLGraalVmMetadataTask::class.java)

// create new source for GraalVM metadata task
if (project.plugins.hasPlugin(GRAALVM_PLUGIN_NAME)) {
val graphQLGraalVmSource = project.extensions.getByType(SourceSetContainer::class.java).create("graphqlGraalVm")
project.tasks.withType(Jar::class.java).configureEach { jarTask ->
jarTask.from(graphQLGraalVmSource.runtimeClasspath)
}
}
}

private fun processExtensionConfiguration(project: Project, extension: GraphQLPluginExtension) {
Expand Down Expand Up @@ -131,9 +155,22 @@ class GraphQLGradlePlugin : Plugin<Project> {
val generateSchemaTask = project.tasks.named(GENERATE_SDL_TASK_NAME, GraphQLGenerateSDLTask::class.java).get()
generateSchemaTask.packages.set(supportedPackages)
}

if (extension.isGraalVmConfigurationAvailable()) {
val supportedPackages = extension.graalVmExtension.packages
if (supportedPackages.isEmpty()) {
throw RuntimeException("Invalid GraphQL graalVm extension configuration - missing required supportedPackages property")
}

val graalVmMetadataTask = project.tasks.named(GRAALVM_METADATA_TASK_NAME, GraphQLGraalVmMetadataTask::class.java).get()
graalVmMetadataTask.packages.set(supportedPackages)
extension.graalVmExtension.mainClassName?.let {
graalVmMetadataTask.mainClassName.set(it)
}
}
}

private fun configureTaskClasspaths(project: Project) {
private fun configureTasks(project: Project) {
val isAndroidProject = project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")
val clientGeneratingTaskNames = mutableListOf<GraphQLGenerateClientTask>()
val testClientGeneratingTaskNames = mutableListOf<GraphQLGenerateTestClientTask>()
Expand Down Expand Up @@ -183,6 +220,43 @@ class GraphQLGradlePlugin : Plugin<Project> {
// we also need to explicitly configure compile dependencies
configureAndroidCompileTasks(project, clientGeneratingTaskNames, testClientGeneratingTaskNames)
}

// auto-configure graphQLGraalVmMetadata task if GraalVM native plugin is applied
if (project.plugins.hasPlugin(GRAALVM_PLUGIN_NAME)) {
project.tasks.withType(GraphQLGraalVmMetadataTask::class.java).configureEach { graalVmMetadataTask ->
// set GraalVM application info
if (project.plugins.hasPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME)) {
project.extensions.findByType(JavaApplication::class.java)?.let { javaApplication ->
javaApplication.mainClass.orNull?.let { mainClass ->
graalVmMetadataTask.mainClassName.convention(mainClass)
}
}
}

// create task dependencies
val compileKotlinTask = project.tasks.findByName("compileKotlin") ?: project.tasks.findByName("compileKotlinJvm")
if (compileKotlinTask != null) {
graalVmMetadataTask.dependsOn(compileKotlinTask)
} else {
project.logger.warn("compileKotlin/compileKotlinJvm tasks not found. Unable to auto-configure the generateSDLTask dependency on compile task.")
}
project.tasks.findByName(NATIVE_COMPILE_TASK_NAME)?.dependsOn(graalVmMetadataTask)

// configure source sets
val sourceSetContainer = project.extensions.getByType(SourceSetContainer::class.java)
val graalVmSource = sourceSetContainer.getByName("graphqlGraalVm")
graalVmSource.resources {
it.setSrcDirs(listOf(graalVmMetadataTask.outputDirectory))
}

val mainSourceSet = sourceSetContainer.getByName("main")
graalVmMetadataTask.source(mainSourceSet.output)
graalVmMetadataTask.projectClasspath.setFrom(mainSourceSet.runtimeClasspath)

val configuration = project.configurations.getAt(GRAALVM_METADATA_CONFIGURATION)
graalVmMetadataTask.pluginClasspath.setFrom(configuration)
}
}
}

private fun configureDefaultProjectSourceSet(project: Project, outputDirectory: DirectoryProperty, targetSourceSet: String = "main") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ open class GraphQLPluginExtension {
GraphQLPluginSchemaExtension()
}

private var graalVmExtensionConfigured: Boolean = false
internal val graalVmExtension: GraphQLPluginGraalVmExtension by lazy {
graalVmExtensionConfigured = true
GraphQLPluginGraalVmExtension()
}

/** Plugin configuration for generating GraphQL client. */
fun client(action: Action<GraphQLPluginClientExtension>) {
action.execute(clientExtension)
Expand All @@ -49,10 +55,16 @@ open class GraphQLPluginExtension {

internal fun isSchemaConfigurationAvailable(): Boolean = schemaExtensionConfigured

internal fun isGraalVmConfigurationAvailable(): Boolean = graalVmExtensionConfigured

/** Plugin configuration for generating GraphQL schema artifact. */
fun schema(action: Action<GraphQLPluginSchemaExtension>) {
action.execute(schemaExtension)
}

fun graalVm(action: Action<GraphQLPluginGraalVmExtension>) {
action.execute(graalVmExtension)
}
}

open class GraphQLPluginClientExtension {
Expand Down Expand Up @@ -102,3 +114,10 @@ open class GraphQLPluginSchemaExtension {
/** List of supported packages that can contain GraphQL schema type definitions. */
var packages: List<String> = emptyList()
}

open class GraphQLPluginGraalVmExtension {
/** List of supported packages that can contain GraphQL schema type definitions. */
var packages: List<String> = emptyList()
/** Application main class name. */
var mainClassName: String? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2023 Expedia, Inc
*
* 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.
*/

package com.expediagroup.graphql.plugin.gradle.actions

import com.expediagroup.graphql.plugin.graalvm.generateGraalVmMetadata
import com.expediagroup.graphql.plugin.gradle.parameters.GenerateGraalVmMetadataParameters
import org.gradle.workers.WorkAction
import java.io.File

abstract class GenerateGraalVmMetadataAction : WorkAction<GenerateGraalVmMetadataParameters> {

/**
* Generate GraphQL GraalVM reachability metadata.
*/
override fun execute() {
val supportedPackages: List<String> = parameters.supportedPackages.get()
val mainClassName: String? = parameters.mainClassName.orNull
val targetDirectory: File = parameters.outputDirectory.get()

generateGraalVmMetadata(targetDirectory = targetDirectory, supportedPackages = supportedPackages, mainClassName = mainClassName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 Expedia, Inc
*
* 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.
*/

package com.expediagroup.graphql.plugin.gradle.parameters

import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.workers.WorkParameters
import java.io.File

/**
* WorkParameters used for generating GraalVM reachability metadata for GraphQL schema.
*/
interface GenerateGraalVmMetadataParameters : WorkParameters {
/** List of supported packages that can contain GraphQL schema type definitions. */
val supportedPackages: ListProperty<String>
/** Main application class name. */
val mainClassName: Property<String>
/** Directory where to store generated reachability metadata. */
val outputDirectory: Property<File>
}
Loading

0 comments on commit f4ba9b4

Please sign in to comment.