main.name.remal.gradle_plugins.plugins.jpms.GenerateModuleInfoTask.kt Maven / Gradle / Ivy
package name.remal.gradle_plugins.plugins.jpms
import groovy.lang.Closure
import groovy.lang.Closure.DELEGATE_FIRST
import groovy.lang.DelegatesTo
import name.remal.ASM_API
import name.remal.accept
import name.remal.buildMap
import name.remal.createParentDirectories
import name.remal.default
import name.remal.escapeRegex
import name.remal.forceDeleteRecursively
import name.remal.gradle_plugins.dsl.BuildTask
import name.remal.gradle_plugins.dsl.artifact.Artifact
import name.remal.gradle_plugins.dsl.artifact.ArtifactsCache
import name.remal.gradle_plugins.dsl.artifact.CachedArtifactsCollection
import name.remal.gradle_plugins.dsl.extensions.addClassesDir
import name.remal.gradle_plugins.dsl.extensions.all
import name.remal.gradle_plugins.dsl.extensions.classesDir
import name.remal.gradle_plugins.dsl.extensions.dirName
import name.remal.gradle_plugins.dsl.extensions.flattenAny
import name.remal.gradle_plugins.dsl.extensions.get
import name.remal.gradle_plugins.dsl.extensions.getRequiredResourceAsStream
import name.remal.gradle_plugins.dsl.extensions.include
import name.remal.gradle_plugins.dsl.extensions.isCompilingSourceSet
import name.remal.gradle_plugins.dsl.extensions.isVersionSet
import name.remal.gradle_plugins.dsl.extensions.javaModuleName
import name.remal.gradle_plugins.dsl.extensions.logDebug
import name.remal.gradle_plugins.dsl.extensions.matches
import name.remal.gradle_plugins.dsl.extensions.readBytes
import name.remal.gradle_plugins.dsl.extensions.requirePlugin
import name.remal.gradle_plugins.dsl.extensions.toConfigureKotlinFunction
import name.remal.gradle_plugins.dsl.extensions.visitFiles
import name.remal.gradle_plugins.dsl.utils.ClassInternalName
import name.remal.gradle_plugins.dsl.utils.ClassName
import name.remal.gradle_plugins.dsl.utils.DependenciesCollectorClassVisitor
import name.remal.gradle_plugins.dsl.utils.DependencyNotation
import name.remal.gradle_plugins.dsl.utils.DependencyNotationMatcher
import name.remal.gradle_plugins.dsl.utils.SkipInvisibleAnnotationsClassVisitor
import name.remal.gradle_plugins.dsl.utils.SkipOuterAndInnerClassesClassVisitor
import name.remal.gradle_plugins.dsl.utils.UsedServicesCollectorClassVisitor
import name.remal.gradle_plugins.dsl.utils.classInternalNameToClassName
import name.remal.gradle_plugins.dsl.utils.classNameToClassInternalName
import name.remal.gradle_plugins.dsl.utils.createDependencyNotation
import name.remal.gradle_plugins.dsl.utils.defaultValue
import name.remal.gradle_plugins.dsl.utils.writeOnce
import name.remal.gradle_plugins.plugins.java.JavaBasePluginId
import name.remal.gradle_plugins.plugins.jpms.RequireModuleMode.REQUIRE_NORMAL
import name.remal.gradle_plugins.plugins.jpms.RequireModuleMode.REQUIRE_STATIC
import name.remal.gradle_plugins.plugins.jpms.RequireModuleMode.REQUIRE_TRANSITIVE
import name.remal.loadProperties
import name.remal.logDebug
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleVersionIdentifier
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.ResolvedModuleVersion
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity.ABSOLUTE
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.compile.AbstractCompile
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes.ACC_MODULE
import org.objectweb.asm.Opcodes.ACC_OPEN
import org.objectweb.asm.Opcodes.ACC_STATIC
import org.objectweb.asm.Opcodes.ACC_STATIC_PHASE
import org.objectweb.asm.Opcodes.ACC_TRANSITIVE
import org.objectweb.asm.Opcodes.V9
import org.objectweb.asm.tree.ModuleExportNode
import org.objectweb.asm.tree.ModuleNode
import org.objectweb.asm.tree.ModuleProvideNode
import org.objectweb.asm.tree.ModuleRequireNode
import java.io.File
import java.nio.charset.StandardCharsets.UTF_8
import kotlin.LazyThreadSafetyMode.NONE
@DslMarker
private annotation class ModuleInfoDslMarker
@BuildTask
@CacheableTask
@ModuleInfoDslMarker
class GenerateModuleInfoTask : DefaultTask() {
companion object {
private val knownClassInternalNamesToModuleName = GenerateModuleInfoTask::class.java.getRequiredResourceAsStream("known-class-module-names.properties")
.use(::loadProperties)
.mapKeys { classNameToClassInternalName(it.key.toString()) }
.mapValues { it.value.toString() }
}
init {
requirePlugin(JavaBasePluginId)
}
@get:Input
var moduleName: String by defaultValue(project::javaModuleName)
@get:Input
var isOpen: Boolean = true
@get:Input
@get:Optional
var mainClassName: String? = null
@Nested
val requires = ModuleInfoRequires()
fun requires(configurer: ModuleInfoRequires.() -> Unit) {
configurer(requires)
}
fun requires(
@DelegatesTo(ModuleInfoRequires::class, strategy = DELEGATE_FIRST)
configurer: Closure<*>
) = requires(configurer.toConfigureKotlinFunction())
@get:Nested
val exports = ModuleInfoExports()
fun exports(configurer: ModuleInfoExports.() -> Unit) {
configurer(exports)
}
fun exports(
@DelegatesTo(ModuleInfoExports::class, strategy = DELEGATE_FIRST)
configurer: Closure<*>
) = exports(configurer.toConfigureKotlinFunction())
@get:Nested
val uses = ModuleInfoUses()
fun uses(configurer: ModuleInfoUses.() -> Unit) {
configurer(uses)
}
fun uses(@DelegatesTo(ModuleInfoUses::class, strategy = DELEGATE_FIRST) configurer: Closure<*>) = uses(configurer.toConfigureKotlinFunction())
@get:OutputDirectory
var destinationDir: File by defaultValue { project.classesDir.resolve("module-info/$dirName") }
@get:InputFiles
@get:PathSensitive(ABSOLUTE)
val classesDirs: ConfigurableFileCollection = project.files()
@get:InputFiles
@get:Classpath
var compileClasspathConfiguration: Configuration? = null
@get:InputFiles
@get:Classpath
var runtimeClasspathConfiguration: Configuration? = null
@get:InputFiles
@get:Classpath
var compileOnlyConfiguration: Configuration? = null
@get:Input
@get:Optional
var classesWithAllowedDynamicServiceLoader: MutableSet = sortedSetOf()
set(value) {
field = value.toSortedSet()
}
@get:Internal
var sourceSet: SourceSet by writeOnce { sourceSet ->
project.classesDir.resolve("module-info/${sourceSet.name}").let { destinationDir ->
this.destinationDir = destinationDir
sourceSet.output.addClassesDir(destinationDir)
}
val compileTasks = project.tasks.withType(AbstractCompile::class.java).matching { it.isCompilingSourceSet(sourceSet) }
compileTasks.all { dependsOn(it) }
classesDirs.from(project.provider {
compileTasks.mapTo(mutableSetOf(), AbstractCompile::getDestinationDir)
})
project.tasks.all(sourceSet.processResourcesTaskName) { dependsOn(it) }
classesDirs.from(project.provider {
sourceSet.output.resourcesDir
})
compileClasspathConfiguration = project.configurations[sourceSet.compileClasspathConfigurationName]
runtimeClasspathConfiguration = project.configurations[sourceSet.runtimeClasspathConfigurationName]
compileOnlyConfiguration = project.configurations[sourceSet.compileOnlyConfigurationName]?.let { conf ->
conf.copy().apply {
isCanBeResolved = true
dependencies.clear()
conf.allDependencies.all { dependencies.add(it) }
}
}
project.tasks[sourceSet.classesTaskName].dependsOn(this)
}
@TaskAction
@Suppress("LongMethod", "ComplexMethod")
protected fun generateModuleInfo() {
didWork = true
val destinationDir = this.destinationDir.apply { forceDeleteRecursively() }
val moduleModifiers: Int = if (isOpen) {
ACC_OPEN
} else {
0
}
val moduleVersion: String? = if (project.isVersionSet) {
project.version.toString()
} else {
null
}
val allPackages = sortedSetOf()
val mainClassNames = sortedSetOf()
val taskClassInternalNames = sortedSetOf()
val usedServicesClassInternalNames = sortedSetOf()
val dependencyClassInternalNames = sortedSetOf()
classesDirs.asFileTree.include("**/*.class").visitFiles { classFileDetails ->
try {
val usedServicesVisitor = UsedServicesCollectorClassVisitor(classesWithAllowedDynamicServiceLoader)
val commonInfoVisitor = CommonInfoClassVisitor(usedServicesVisitor)
val dependenciesClassVisitor = DependenciesCollectorClassVisitor(commonInfoVisitor)
var classVisitor: ClassVisitor = dependenciesClassVisitor
classVisitor = SkipInvisibleAnnotationsClassVisitor(classVisitor)
classVisitor = SkipOuterAndInnerClassesClassVisitor(classVisitor)
val bytecode = classFileDetails.readBytes()
val classReader = ClassReader(bytecode)
classReader.accept(classVisitor)
commonInfoVisitor.classInternalName?.also { classInternalName ->
taskClassInternalNames.add(classInternalName)
val className = classInternalNameToClassName(classInternalName)
allPackages.add(className.substringBeforeLast('.', ""))
if (commonInfoVisitor.hasMainMethod) {
mainClassNames.add(className)
}
}
usedServicesClassInternalNames.addAll(usedServicesVisitor.usedServiceClassInternalNames)
dependencyClassInternalNames.addAll(dependenciesClassVisitor.dependencyClassInternalNames)
} catch (e: Throwable) {
throw GradleException("Error processing $classFileDetails", e)
}
}
dependencyClassInternalNames.removeAll(taskClassInternalNames)
val packagesToExport = exports.filterPackageNames(allPackages)
val mainClassName: ClassName? = this.mainClassName.let { mainClassName ->
if (mainClassName != null) {
if (mainClassName !in mainClassNames) {
throw IllegalStateException("Main class '$mainClassName' can't be found in parsed main classes: ${mainClassNames.joinToString(", ")}")
} else {
return@let mainClassName
}
} else if (mainClassNames.isEmpty()) {
return@let null
} else if (mainClassNames.size >= 2) {
throw IllegalStateException(
"Main class should be set explicitly, as several main classes has been found: ${
mainClassNames.joinToString(
", "
)
}"
)
} else {
return@let mainClassNames.single()
}
}
val requiredModules: Set = dependencyClassInternalNames.let { classInternalNames ->
if (classInternalNames.isEmpty()) return@let emptySet()
val resolvedArtifacts = compileClasspathConfiguration?.resolvedConfiguration?.resolvedArtifacts.default(emptySet())
.plus(runtimeClasspathConfiguration?.resolvedConfiguration?.resolvedArtifacts.default(emptySet()))
val artifactsCollection = CachedArtifactsCollection(resolvedArtifacts.map(ResolvedArtifact::getFile))
val moduleToVersion: Map by lazy(NONE) {
buildMap {
artifactsCollection.artifacts.forEach { artifact ->
val moduleName = artifact.javaModuleName ?: return@forEach
val version = resolvedArtifacts.firstOrNull { it.file.absoluteFile == artifact.file.absoluteFile }
?.moduleVersion
?.let(ResolvedModuleVersion::getId)
?.let(ModuleVersionIdentifier::getVersion)
?.takeUnless(String::isBlank)
logDebug("module: {}, version: {}", moduleName, version)
put(moduleName, version)
}
}
}
val runtimeModuleNames: Set by lazy(NONE) {
val conf = runtimeClasspathConfiguration ?: return@lazy emptySet()
conf.files.asSequence()
.map(ArtifactsCache::get)
.mapNotNull(Artifact::javaModuleName)
.toHashSet()
}
val staticModuleNames: Set by lazy(NONE) {
val conf = compileOnlyConfiguration ?: return@lazy emptySet()
conf.files.asSequence()
.map(ArtifactsCache::get)
.mapNotNull(Artifact::javaModuleName)
.filter { it !in runtimeModuleNames }
.toHashSet()
}
classInternalNames.asSequence()
.mapNotNull map@{ classInternalName ->
if (classInternalName.startsWith("java/")) {
knownClassInternalNamesToModuleName[classInternalName]?.let { moduleName ->
return@map ModuleInfo(moduleName, REQUIRE_NORMAL)
}
} else {
artifactsCollection.getArtifactForEntry("$classInternalName.class")?.let { artifact ->
artifact.javaModuleName?.let { moduleName ->
val requireMode: RequireModuleMode = if (moduleName in staticModuleNames) {
REQUIRE_STATIC
} else {
REQUIRE_NORMAL
}
return@map ModuleInfo(moduleName, requireMode, moduleToVersion[moduleName])
}
}
knownClassInternalNamesToModuleName[classInternalName]?.let { moduleName ->
return@map ModuleInfo(moduleName, REQUIRE_STATIC)
}
}
return@map null
}
.toSortedSet()
.apply {
mapOf(
requires.staticModuleNames to REQUIRE_STATIC,
requires.normalModuleNames to REQUIRE_NORMAL,
requires.transitiveModuleNames to REQUIRE_TRANSITIVE
).forEach { modulesNames, requireMode ->
modulesNames.forEach { moduleName ->
val moduleInfo = ModuleInfo(moduleName, requireMode, moduleToVersion[moduleName])
removeIf { it.moduleName == moduleInfo.moduleName }
add(moduleInfo)
}
}
mapOf(
requires.staticDependencyNotations to REQUIRE_STATIC,
requires.normalDependencyNotations to REQUIRE_NORMAL,
requires.transitiveDependencyNotations to REQUIRE_TRANSITIVE
).forEach { dependencyNotations, requireMode ->
dependencyNotations.asSequence()
.map(::DependencyNotationMatcher)
.flatMap { matcher ->
resolvedArtifacts.asSequence()
.filter { matcher.matches(it) }
}
.distinct()
.forEach { resolvedArtifact ->
ArtifactsCache[resolvedArtifact.file].javaModuleName?.let { moduleName ->
val moduleInfo = ModuleInfo(moduleName, requireMode, resolvedArtifact.moduleVersion.id.version)
removeIf { it.moduleName == moduleInfo.moduleName }
add(moduleInfo)
}
}
}
}
.apply {
removeIf { it.moduleName == "java.base" }
}
}
val excludedModuleNames: Set = hashSetOf().apply {
val dependencyNotationsToExclude = requires.dependencyNotationsToExclude
if (dependencyNotationsToExclude.isNotEmpty()) {
val resolvedArtifacts = compileClasspathConfiguration?.resolvedConfiguration?.resolvedArtifacts.default(emptySet())
requires.dependencyNotationsToExclude.asSequence()
.map(::DependencyNotationMatcher)
.flatMap { matcher ->
resolvedArtifacts.asSequence()
.filter(matcher::matches)
}
.mapNotNull { ArtifactsCache.get(it.file).javaModuleName }
.forEach { add(it) }
}
addAll(requires.moduleNamesToExclude)
}
val moduleNode = ModuleNode(moduleName, moduleModifiers, moduleVersion).apply {
mainClass = mainClassName?.let(::classNameToClassInternalName)
exports = mutableListOf()
packagesToExport.forEach { pkg ->
exports.add(ModuleExportNode(pkg, 0, null))
}
uses = hashSetOf().apply {
usedServicesClassInternalNames.forEach { add(classInternalNameToClassName(it)) }
addAll([email protected])
}.toList().sorted()
requires = mutableListOf()
requiredModules.forEach { requiredModule ->
val moduleName = requiredModule.moduleName
if (moduleName in excludedModuleNames) return@forEach
val modifiers: Int = when (requiredModule.requireMode) {
REQUIRE_NORMAL -> 0
REQUIRE_TRANSITIVE -> ACC_TRANSITIVE
REQUIRE_STATIC -> ACC_STATIC_PHASE
}
requires.add(ModuleRequireNode(moduleName, modifiers, requiredModule.version))
}
provides = mutableListOf()
providedImplementations.forEach { serviceName, implementations ->
provides.add(ModuleProvideNode(
classNameToClassInternalName(serviceName),
implementations.map { classNameToClassInternalName(it) }
))
}
}
val classWriter = ClassWriter(0)
classWriter.visit(V9, ACC_MODULE, "module-info", null, null, null)
moduleNode.accept(classWriter)
classWriter.visitEnd()
destinationDir.resolve("module-info.class")
.createParentDirectories()
.writeBytes(classWriter.toByteArray())
}
private val providedImplementations: Map>
get() = sortedMapOf>().apply {
classesDirs.asFileTree.include("META-INF/services/*").visitFiles { serviceFileDetails ->
try {
val serviceName = serviceFileDetails.name
val implementations = computeIfAbsent(serviceName, { mutableSetOf() })
String(serviceFileDetails.readBytes(), UTF_8).splitToSequence('\n')
.map { it.substringBefore('#') }
.map(String::trim)
.filter(String::isNotEmpty)
.forEach { implementations.add(it) }
} catch (e: Throwable) {
throw GradleException("Error processing $serviceFileDetails", e)
}
}
}
}
@ModuleInfoDslMarker
data class ModuleInfoRequires(
@get:Input
@get:Optional
val normalModuleNames: MutableSet = sortedSetOf(),
@get:Internal
val normalDependencies: MutableSet = mutableSetOf(),
@get:Input
@get:Optional
val transitiveModuleNames: MutableSet = sortedSetOf(),
@get:Internal
val transitiveDependencies: MutableSet = mutableSetOf(),
@get:Input
@get:Optional
val staticModuleNames: MutableSet = sortedSetOf(),
@get:Internal
val staticDependencies: MutableSet = mutableSetOf(),
@get:Input
@get:Optional
val moduleNamesToExclude: MutableSet = sortedSetOf(),
@get:Internal
val dependenciesToExclude: MutableSet = mutableSetOf()
) {
fun add(moduleName: String) {
normalModuleNames.add(moduleName)
}
fun addDependency(dependency: Any) {
normalDependencies.addAll(dependency.flattenAny())
}
fun addDependencies(vararg dependencies: Any) = addDependency(dependencies)
fun addDependencies(dependencies: Iterable) = addDependency(dependencies)
@get:Input
@get:Optional
val normalDependencyNotations: Set
get() = normalDependencies.asSequence()
.map(::createDependencyNotation)
.map(DependencyNotation::toString)
.toSortedSet()
fun addTransitive(moduleName: String) {
transitiveModuleNames.add(moduleName)
}
fun addTransitiveDependency(dependency: Any) {
transitiveDependencies.addAll(dependency.flattenAny())
}
fun addTransitiveDependencies(vararg dependencies: Any) = addTransitiveDependency(dependencies)
fun addTransitiveDependencies(dependencies: Iterable) = addTransitiveDependency(dependencies)
@get:Input
@get:Optional
val transitiveDependencyNotations: Set
get() = transitiveDependencies.asSequence()
.map(::createDependencyNotation)
.map(DependencyNotation::toString)
.toSortedSet()
fun addStatic(moduleName: String) {
staticModuleNames.add(moduleName)
}
fun addStaticDependency(dependency: Any) {
staticDependencies.addAll(dependency.flattenAny())
}
fun addStaticDependencies(vararg dependencies: Any) = addStaticDependency(dependencies)
fun addStaticDependencies(dependencies: Iterable) = addStaticDependency(dependencies)
@get:Input
@get:Optional
val staticDependencyNotations: Set
get() = staticDependencies.asSequence()
.map(::createDependencyNotation)
.map(DependencyNotation::toString)
.toSortedSet()
fun exclude(moduleName: String) {
moduleNamesToExclude.add(moduleName)
}
fun excludeDependency(dependency: Any) {
dependenciesToExclude.addAll(dependency.flattenAny())
}
fun excludeDependencies(vararg dependencies: Any) = excludeDependency(dependencies)
fun excludeDependencies(dependencies: Iterable) = excludeDependency(dependencies)
@get:Input
@get:Optional
val dependencyNotationsToExclude: Set
get() = dependenciesToExclude.asSequence()
.map(::createDependencyNotation)
.map(DependencyNotation::toString)
.toSortedSet()
}
@ModuleInfoDslMarker
data class ModuleInfoExports(
@get:Input
@get:Optional
var includes: MutableSet = sortedSetOf(),
@get:Input
@get:Optional
var excludes: MutableSet = sortedSetOf(
"*.internal.*",
"*.shaded.*",
"*.shadow.*"
)
) {
fun filterPackageNames(packageNames: Iterable): List {
val includeRegexps = includes.toRegexps()
val excludeRegexps = excludes.toRegexps()
return packageNames.asSequence()
.distinct()
.filter { pkg ->
if (includeRegexps.isNotEmpty() && includeRegexps.none(pkg::matches)) {
logDebug("skip export: {} - not in includes", pkg)
return@filter false
}
if (excludeRegexps.isNotEmpty() && excludeRegexps.any(pkg::matches)) {
logDebug("skip export: {} - in excludes", pkg)
return@filter false
}
logDebug("export: {}", pkg)
return@filter true
}
.sorted()
.toList()
}
private fun Iterable.toRegexps(): List = map { pattern ->
if (pattern.trim('*', '.').isEmpty()) {
return@map matchAll
}
val normalizedPattern = pattern.replace(manyStars, "*")
return@map Regex(buildString {
if (normalizedPattern.startsWith("*.")) {
append("(?:[^.]+\\.)*")
}
append(
normalizedPattern.trim('*', '.')
.split(".*.")
.map(::escapeRegex)
.joinToString("(?:\\.[^.]+)*\\.")
)
if (normalizedPattern.endsWith(".*")) {
append("(?:\\.[^.]+)*")
}
})
}
companion object {
private val matchAll = Regex(".*")
private val manyStars = Regex("\\*{2,}")
}
}
@ModuleInfoDslMarker
data class ModuleInfoUses(
@get:Input
@get:Optional
val services: MutableSet = sortedSetOf()
) {
fun add(service: ClassName) {
services.add(service)
}
fun add(service: Class<*>) {
services.add(service.name)
}
}
private class CommonInfoClassVisitor(delegate: ClassVisitor?) : ClassVisitor(ASM_API, delegate) {
var classInternalName: ClassInternalName? = null
var hasMainMethod: Boolean = false
override fun visit(version: Int, access: Int, name: ClassInternalName, signature: String?, superName: ClassInternalName?, interfaces: Array?) {
classInternalName = name
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor? {
if ((access and ACC_STATIC) != 0
&& name == "main"
&& descriptor == "([Ljava/lang/String;)V"
) {
hasMainMethod = true
}
return super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
private data class ModuleInfo(
val moduleName: String,
val requireMode: RequireModuleMode = REQUIRE_NORMAL,
val version: String? = null
) : Comparable {
override fun equals(other: Any?) = other is ModuleInfo && moduleName == other.moduleName
override fun hashCode() = moduleName.hashCode()
override fun compareTo(other: ModuleInfo): Int {
requireMode.compareTo(other.requireMode).let { if (it != 0) return it }
return moduleName.compareTo(other.moduleName)
}
}
private enum class RequireModuleMode {
REQUIRE_TRANSITIVE,
REQUIRE_NORMAL,
REQUIRE_STATIC
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy