godot-kotlin-jvm
godot-kotlin-jvm copied to clipboard
Add Support for Excluding Libraries from the Fat JAR
Certain libraries, such as JCE (Java Cryptography Extension), require their JARs to remain signed for proper functionality. When these JARs are merged into the main.jar during the fat JAR creation process, they lose their signatures, causing runtime errors on JVMs that enforce signature validation (e.g., Oracle-based JVMs like GraalVM). OpenJDK-based JVMs do not have this issue, making this a JVM-specific edge case.
Improvement
- Add functionality to explicitly exclude specified libraries from being bundled into the fat JAR. These libraries would instead remain as separate JAR files in the libs directory, allowing their signatures to stay intact and be used correctly.
Use Case
- Avoids cryptographic library errors caused by invalidated signatures (e.g., when using GraalVM with BouncyCastle jars).
- Provides developers more flexibility to handle dependencies that have unique requirements, such as JCE libraries or other signed JARs.
Current workaround
- Exclude signed libraries from being included in the fat jar
- Add libraries to the output directory on build
- Manage loading libraries dynamically on runtime programatically, depending on if they are present in classpath (GraalVM) or libs folder (editor)
tasks.register<Copy>("copySignedBouncyCastleJars") {
dependsOn("build") // Ensure this runs after the build task
val outputDir = layout.buildDirectory.dir("libs").get().asFile
from(configurations.runtimeClasspath.get().filter {
it.name.contains("bcprov") || it.name.contains("bcpkix")
})
into(outputDir)
}
tasks.named("build") {
finalizedBy("copySignedBouncyCastleJars")
}
tasks.named<ShadowJar>("shadowJar") {
exclude("**/org/bouncycastle/**")
}
tasks.withType<Exec>().configureEach {
if (name == "createGraalNativeImage")
{
doFirst {
val libsDir = layout.buildDirectory.dir("libs").get().asFile
val additionalJars = libsDir.listFiles { file ->
file.name.endsWith(".jar") && (file.name.contains("bcprov") || file.name.contains("bcpkix"))
} ?: emptyArray()
val classpathJars = additionalJars.joinToString(":") { it.absolutePath }
val arguments = commandLine as MutableList<String>
val classpathIndex = arguments.indexOf("-cp")
if (classpathIndex != -1 && classpathIndex + 1 < arguments.size)
{
val existingClasspath = arguments[classpathIndex + 1]
arguments[classpathIndex + 1] = "$existingClasspath:$classpathJars"
}
commandLine = arguments
}
}
}
package util
import dev.whyoleg.cryptography.CryptographyProvider
import dev.whyoleg.cryptography.providers.jdk.JDK
import io.github.oshai.kotlinlogging.KotlinLogging
import java.io.File
import java.net.URLClassLoader
import java.security.Security
object CryptoJarManager
{
private val logger = KotlinLogging.logger {}
private val REQUIRED_JARS = listOf(
"bcpkix-jdk18on-1.79.jar", "bcprov-jdk18on-1.79.jar"
)
private var directoryPath = "./build/libs"
init
{
val isProviderAvailable = try
{
Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider")
true
}
catch (e: ClassNotFoundException)
{
false
}
if (isProviderAvailable)
{
logger.info { "BouncyCastleProvider is already available in the classpath. Dynamic loading skipped." }
}
else
{
logger.info { "BouncyCastleProvider not found in classpath. Attempting to load JARs dynamically." }
loadJarsDynamically()
}
}
private fun loadJarsDynamically()
{
val jarDir = File(directoryPath).absoluteFile
if (!jarDir.exists() || !jarDir.isDirectory)
{
throw IllegalArgumentException("Directory ${jarDir.path} does not exist or is not a directory")
}
val jarFiles = jarDir.listFiles { file ->
file.extension == "jar" && REQUIRED_JARS.contains(file.name)
} ?: throw IllegalStateException("No required JAR files found in directory: ${jarDir.path}")
val urls = jarFiles.map { it.toURI().toURL() }.toTypedArray()
val classLoader = URLClassLoader(urls, this::class.java.classLoader)
logger.info { "Custom ClassLoader loaded the following JARs:" }
jarFiles.forEach { logger.info { "- ${it.name}" } }
try
{
val taskClass = classLoader.loadClass("org.bouncycastle.jce.provider.BouncyCastleProvider")
val providerInstance = taskClass.getDeclaredConstructor().newInstance() as java.security.Provider
Security.addProvider(providerInstance)
logger.info { "BouncyCastle provider registered dynamically in isolated context." }
}
catch (e: Exception)
{
logger.error { "Failed to load or register BouncyCastle provider: ${e.message}" }
e.printStackTrace()
}
}
val provider: CryptographyProvider
get()
{
val bouncyCastleProvider = Security.getProvider("BC") ?: throw IllegalStateException("BouncyCastle provider not loaded")
return CryptographyProvider.JDK(bouncyCastleProvider)
}
}
Gradle context
dependencies {
implementation("dev.whyoleg.cryptography:cryptography-core:0.4.0")
implementation("dev.whyoleg.cryptography:cryptography-provider-jdk:0.4.0")
implementation("org.bouncycastle:bcprov-jdk18on:1.79")
implementation("org.bouncycastle:bcpkix-jdk18on:1.79")
}