-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make cel-java work in Graal native images (#23)
Introduces a Gradle plugin to scan the compiled classes and configuration-dependencies for classes that require an entry in `reflection-config.json`. With the generated `reflection-config.json` + `native-image.properties` the Graal `native-image` tool has enough information to allow reflection for google-protobuf generated files.
- Loading branch information
Showing
18 changed files
with
401 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
|
||
# Gradle | ||
build/ | ||
/.gradle | ||
.gradle/ | ||
|
||
# IDE | ||
/.idea | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright (C) 2021 The Authors of CEL-Java | ||
* | ||
* 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 | ||
* | ||
* http://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. | ||
*/ | ||
|
||
plugins { | ||
`kotlin-dsl` | ||
id("com.gradle.plugin-publish") version "0.15.0" | ||
id("com.diffplug.spotless") version "5.14.0" | ||
} | ||
|
||
repositories { | ||
gradlePluginPortal() | ||
mavenCentral() | ||
} | ||
|
||
group = "org.projectnessie.cel.build" | ||
|
||
version = file("../version.txt").readText().trim() | ||
|
||
val versionAsm = "9.1" | ||
val versionProtobufPlugin = "0.8.16" | ||
|
||
dependencies { | ||
implementation("org.ow2.asm:asm:$versionAsm") | ||
implementation("com.google.protobuf:protobuf-gradle-plugin:$versionProtobufPlugin") | ||
} | ||
|
||
java { | ||
withJavadocJar() | ||
withSourcesJar() | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
|
||
gradlePlugin { | ||
plugins { | ||
create("reflectionconfig") { | ||
id = "org.projectnessie.cel.reflectionconfig" | ||
implementationClass = "org.projectnessie.cel.tools.plugins.ReflectionConfigPlugin" | ||
} | ||
} | ||
} | ||
|
||
pluginBundle { | ||
vcsUrl = "https://github.com/projectnessie/cel-java/" | ||
|
||
plugins { named("reflectionconfig") {} } | ||
} | ||
|
||
spotless { | ||
kotlinGradle { | ||
ktfmt().googleStyle() | ||
// licenseHeaderFile(rootProject.file("../gradle/license-header-java.txt"), "") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/* | ||
* Copyright (C) 2021 The Authors of CEL-Java | ||
* | ||
* 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 | ||
* | ||
* http://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. | ||
*/ |
45 changes: 45 additions & 0 deletions
45
build-tools/src/main/kotlin/org/projectnessie/cel/tools/plugins/ReflectionConfigExtension.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* Copyright (C) 2021 The Authors of CEL-Java | ||
* | ||
* 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 | ||
* | ||
* http://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 org.projectnessie.cel.tools.plugins | ||
|
||
import org.gradle.api.Project | ||
|
||
/** Configuration that specifies which classes shall be mentioned in generated | ||
* reflection-config.json files. | ||
* | ||
* A class will end in a generated reflection-config.json file, if its superclass | ||
* matches one of the regex patterns in `classExtendsPatterns` or if one of its directly | ||
* implemented interfaces matches one of the regex patterns in `classImplementsPatterns`. | ||
* | ||
* By default the plugin scans the classes by the project's source-sets "main" + "test". | ||
* It can optionally consider resolvable configurations specified in `includeConfigurations`. | ||
* | ||
* Note that the plugin scans the classes using "asm" and does not consider any indirect | ||
* superclass nor does it consider any implicitly implementated interface. | ||
*/ | ||
open class ReflectionConfigExtension(project: Project) { | ||
/** A superclass must match one of these regular expressions. If this list is empty, all | ||
* superclasses will match. */ | ||
var classExtendsPatterns = project.objects.listProperty(String::class.java) | ||
|
||
/** Directly implemented interfaces must match one of these regular expressions. If this list | ||
* is empty, the class' directly implemented interfaces are not considered. */ | ||
var classImplementsPatterns = project.objects.listProperty(String::class.java) | ||
|
||
/** Resolvable configuration(s) that should be scanned for classes matching the patterns as well. */ | ||
var includeConfigurations = project.objects.listProperty(String::class.java) | ||
} |
63 changes: 63 additions & 0 deletions
63
build-tools/src/main/kotlin/org/projectnessie/cel/tools/plugins/ReflectionConfigPlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright (C) 2021 The Authors of CEL-Java | ||
* | ||
* 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 | ||
* | ||
* http://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 org.projectnessie.cel.tools.plugins | ||
|
||
import org.gradle.api.Plugin | ||
import org.gradle.api.Project | ||
import org.gradle.api.plugins.JavaLibraryPlugin | ||
import org.gradle.api.tasks.SourceSet | ||
import org.gradle.api.tasks.SourceSetContainer | ||
import org.gradle.api.tasks.compile.JavaCompile | ||
|
||
/** Generates `reflection-config.json` files from compiled classes. */ | ||
@Suppress("unused") | ||
class ReflectionConfigPlugin : Plugin<Project> { | ||
override fun apply(project: Project): Unit = project.run { | ||
plugins.apply(JavaLibraryPlugin::class.java) | ||
|
||
extensions.create("reflectionConfig", ReflectionConfigExtension::class.java, this) | ||
|
||
configureFor(SourceSet.MAIN_SOURCE_SET_NAME, this) | ||
configureFor(SourceSet.TEST_SOURCE_SET_NAME, this) | ||
} | ||
|
||
private fun configureFor(sourceSetName: String, project: Project) = project.run { | ||
extensions.getByType(SourceSetContainer::class.java).named(sourceSetName) { | ||
val dirName = project.buildDir.resolve("generated/resource/reflect-conf/$sourceSetName") | ||
resources.srcDir(dirName) | ||
|
||
val compileJava = tasks.named(compileJavaTaskName, JavaCompile::class.java) | ||
val genRefCfg = tasks.register(getTaskName("generate", "reflectionConfig"), ReflectionConfigTask::class.java) | ||
genRefCfg.configure { | ||
val e = project.extensions.getByType(ReflectionConfigExtension::class.java) | ||
|
||
setName.set(sourceSetName) | ||
classesFolder.set(compileJava.get().destinationDirectory) | ||
outputDirectory.set(file(dirName)) | ||
|
||
classExtendsPatterns.set(e.classExtendsPatterns) | ||
classImplementsPatterns.set(e.classImplementsPatterns) | ||
includeConfigurations.set(e.includeConfigurations) | ||
|
||
dependsOn(compileJava) | ||
} | ||
tasks.named(processResourcesTaskName) { | ||
dependsOn(genRefCfg) | ||
} | ||
} | ||
} | ||
} |
159 changes: 159 additions & 0 deletions
159
build-tools/src/main/kotlin/org/projectnessie/cel/tools/plugins/ReflectionConfigTask.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/* | ||
* Copyright (C) 2021 The Authors of CEL-Java | ||
* | ||
* 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 | ||
* | ||
* http://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 org.projectnessie.cel.tools.plugins | ||
|
||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.GradleException | ||
import org.gradle.api.tasks.CacheableTask | ||
import org.gradle.api.tasks.Input | ||
import org.gradle.api.tasks.InputFiles | ||
import org.gradle.api.tasks.OutputDirectory | ||
import org.gradle.api.tasks.PathSensitive | ||
import org.gradle.api.tasks.PathSensitivity | ||
import org.gradle.api.tasks.TaskAction | ||
import org.objectweb.asm.ClassReader | ||
import org.objectweb.asm.ClassVisitor | ||
import org.objectweb.asm.Opcodes | ||
import org.objectweb.asm.Type | ||
import java.io.BufferedInputStream | ||
import java.io.File | ||
import java.io.FileInputStream | ||
import java.io.InputStream | ||
import java.util.jar.JarInputStream | ||
import java.util.regex.Pattern | ||
|
||
@CacheableTask | ||
open class ReflectionConfigTask : DefaultTask() { | ||
@InputFiles | ||
@PathSensitive(PathSensitivity.RELATIVE) | ||
val classesFolder = project.objects.directoryProperty() | ||
|
||
@OutputDirectory | ||
val outputDirectory = project.objects.directoryProperty() | ||
|
||
@Input | ||
val classExtendsPatterns = project.objects.listProperty(String::class.java) | ||
|
||
@Input | ||
val classImplementsPatterns = project.objects.listProperty(String::class.java) | ||
|
||
@Input | ||
val setName = project.objects.property(String::class.java) | ||
|
||
@Input | ||
var includeConfigurations = project.objects.listProperty(String::class.java) | ||
|
||
@TaskAction | ||
fun generateReflectionConfig() { | ||
val extPats = classExtendsPatterns.get().map { s -> Pattern.compile(s) }.toList() | ||
val implPats = classImplementsPatterns.get().map { s -> Pattern.compile(s) }.toList() | ||
|
||
val baseDir = outputDirectory.get().file("META-INF/native-image/${project.group}/${project.name}/${setName.get()}").asFile | ||
if (!baseDir.isDirectory) { | ||
if (!baseDir.mkdirs()) { | ||
throw GradleException("Could not create directory '$baseDir'") | ||
} | ||
} | ||
|
||
val classFolderStream = classesFolder.get().asFileTree.filter { f -> f.name.endsWith(".class") }.mapNotNull { file -> | ||
processClassFile(file, extPats, implPats) | ||
} | ||
|
||
val dependenciesStream = includeConfigurations.get().map { cfg -> project.configurations.getByName(cfg) } | ||
.flatMap { cfg -> cfg.resolve() } | ||
.flatMap { file -> | ||
val classNames = mutableListOf<String>() | ||
JarInputStream(FileInputStream(file.absoluteFile)).use { | ||
while (true) { | ||
val n = it.nextJarEntry | ||
if (n == null) { | ||
break | ||
} | ||
if (n.name.endsWith(".class")) { | ||
val clsName = processClassFile(it, extPats, implPats) | ||
if (clsName != null) { | ||
classNames.add(clsName) | ||
} | ||
} | ||
} | ||
} | ||
classNames | ||
} | ||
|
||
baseDir.resolve("native-image.properties").writeText( | ||
"# This file is generated for ${project.group}:${project.name}:${project.version}.\n" + | ||
"# Contains classes \n" + | ||
"# with superclass: ${extPats.joinToString(",\n# ", "\n# ")}\n" + | ||
"# implementing interfaces: ${implPats.joinToString(",\n# ", "\n# ")}\n" + | ||
"Args = -H:ReflectionConfigurationResources=\${.}/reflection-config.json\n") | ||
|
||
baseDir.resolve("reflection-config.json").writeText( | ||
(dependenciesStream + classFolderStream).map { clsName -> | ||
""" { | ||
| "name" : "$clsName", | ||
| "allDeclaredConstructors" : true, | ||
| "allPublicConstructors" : true, | ||
| "allDeclaredMethods" : true, | ||
| "allPublicMethods" : true, | ||
| "allDeclaredFields" : true, | ||
| "allPublicFields" : true | ||
| }""".trimMargin() | ||
}.joinToString(",\n", "[\n", "\n]")) | ||
} | ||
|
||
private fun processClassFile(file: File, extPats: List<Pattern>, implPats: List<Pattern>): String? { | ||
BufferedInputStream(FileInputStream(file)).use { input -> | ||
return processClassFile(input, extPats, implPats) | ||
} | ||
} | ||
|
||
private fun processClassFile(input: InputStream, extPats: List<Pattern>, implPats: List<Pattern>): String? { | ||
val classVisitor = ClsVisit() | ||
|
||
ClassReader(input).accept(classVisitor, ClassReader.SKIP_CODE + ClassReader.SKIP_FRAMES + ClassReader.SKIP_DEBUG) | ||
|
||
if (classVisitor.extends != null && (matchesPattern(classVisitor.extends!!, extPats) || matchesPattern(classVisitor.implements, implPats))) { | ||
return classVisitor.className | ||
} | ||
return null | ||
} | ||
|
||
private fun matchesPattern(ifs: List<String>, pats: List<Pattern>): Boolean { | ||
return ifs.filter { ifName -> matchesPattern(ifName, pats) }.any() | ||
} | ||
|
||
private fun matchesPattern(cls: String, pats: List<Pattern>): Boolean { | ||
if (pats.isEmpty()) { | ||
return true | ||
} | ||
return pats.filter { p -> p.matcher(cls).matches() }.any() | ||
} | ||
|
||
private class ClsVisit : ClassVisitor(Opcodes.ASM9) { | ||
var className: String? = null | ||
var extends: String? = null | ||
var implements: List<String> = listOf() | ||
|
||
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>) { | ||
if (access.and(Opcodes.ACC_PUBLIC) != 0) { | ||
className = Type.getObjectType(name).className | ||
extends = Type.getObjectType(superName).className | ||
implements = interfaces.map { i -> Type.getObjectType(i).className }.toList() | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.