diff options
Diffstat (limited to 'atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt')
-rw-r--r-- | atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt | 311 |
1 files changed, 208 insertions, 103 deletions
diff --git a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt index bb619a5..b77e95b 100644 --- a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt +++ b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt @@ -14,22 +14,33 @@ import org.gradle.api.tasks.compile.* import org.gradle.api.tasks.testing.* import org.gradle.jvm.tasks.* import org.jetbrains.kotlin.gradle.dsl.* +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile import org.jetbrains.kotlin.gradle.plugin.* import java.io.* import java.util.* import java.util.concurrent.* +import org.jetbrains.kotlin.gradle.targets.js.* +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import org.jetbrains.kotlin.gradle.tasks.* +import org.jetbrains.kotlinx.atomicfu.gradle.* private const val EXTENSION_NAME = "atomicfu" private const val ORIGINAL_DIR_NAME = "originalClassesDir" private const val COMPILE_ONLY_CONFIGURATION = "compileOnly" private const val IMPLEMENTATION_CONFIGURATION = "implementation" private const val TEST_IMPLEMENTATION_CONFIGURATION = "testImplementation" +// If the project uses KGP <= 1.6.20, only JS IR compiler plugin is available, and it is turned on via setting this property. +// The property is supported for backwards compatibility. +private const val ENABLE_JS_IR_TRANSFORMATION_LEGACY = "kotlinx.atomicfu.enableIrTransformation" +private const val ENABLE_JS_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJsIrTransformation" +private const val ENABLE_JVM_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJvmIrTransformation" open class AtomicFUGradlePlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val pluginVersion = rootProject.buildscript.configurations.findByName("classpath") ?.allDependencies?.find { it.name == "atomicfu-gradle-plugin" }?.version extensions.add(EXTENSION_NAME, AtomicFUPluginExtension(pluginVersion)) + applyAtomicfuCompilerPlugin() configureDependencies() configureTasks() } @@ -43,12 +54,13 @@ private fun Project.configureDependencies() { ) dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JVM, version)) } - withPluginWhenEvaluatedDependencies("kotlin2js") { version -> + withPluginWhenEvaluatedDependencies("org.jetbrains.kotlin.js") { version -> dependencies.add( if (config.transformJs) COMPILE_ONLY_CONFIGURATION else IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JS, version) ) dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JS, version)) + addCompilerPluginDependency() } withPluginWhenEvaluatedDependencies("kotlin-multiplatform") { version -> configureMultiplatformPluginDependencies(version) @@ -59,7 +71,9 @@ private fun Project.configureTasks() { val config = config withPluginWhenEvaluated("kotlin") { if (config.transformJvm) { - configureTransformTasks("compileTestKotlin") { sourceSet, transformedDir, originalDir -> + // skip transformation task if ir transformation is enabled + if (rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) return@withPluginWhenEvaluated + configureJvmTransformation("compileTestKotlin") { sourceSet, transformedDir, originalDir -> createJvmTransformTask(sourceSet).configureJvmTask( sourceSet.compileClasspath, sourceSet.classesTaskName, @@ -70,20 +84,88 @@ private fun Project.configureTasks() { } } } - withPluginWhenEvaluated("kotlin2js") { - if (config.transformJs) { - configureTransformTasks("compileTestKotlin2Js") { sourceSet, transformedDir, originalDir -> - createJsTransformTask(sourceSet).configureJsTask( - sourceSet.classesTaskName, - transformedDir, - originalDir, - config - ) + withPluginWhenEvaluated("org.jetbrains.kotlin.js") { + if (config.transformJs) configureJsTransformation() + } + withPluginWhenEvaluated("kotlin-multiplatform") { + configureMultiplatformTransformation() + } +} + +private data class KotlinVersion(val major: Int, val minor: Int, val patch: Int) + +private fun Project.getKotlinVersion(): KotlinVersion { + val kotlinVersion = getKotlinPluginVersion() + val (major, minor) = kotlinVersion + .split('.') + .take(2) + .map { it.toInt() } + val patch = kotlinVersion.substringAfterLast('.').substringBefore('-').toInt() + return KotlinVersion(major, minor, patch) +} + +private fun KotlinVersion.atLeast(major: Int, minor: Int, patch: Int) = + this.major == major && (this.minor == minor && this.patch >= patch || this.minor > minor) || this.major > major + +// kotlinx-atomicfu compiler plugin is available for KGP >= 1.6.20 +private fun Project.isCompilerPluginAvailable() = getKotlinVersion().atLeast(1, 6, 20) + +private fun Project.applyAtomicfuCompilerPlugin() { + val kotlinVersion = getKotlinVersion() + // for KGP >= 1.7.20: + // compiler plugin for JS IR is applied via the property `kotlinx.atomicfu.enableJsIrTransformation` + // compiler plugin for JVM IR is applied via the property `kotlinx.atomicfu.enableJvmIrTransformation` + if (kotlinVersion.atLeast(1, 7, 20)) { + plugins.apply(AtomicfuKotlinGradleSubplugin::class.java) + extensions.getByType(AtomicfuKotlinGradleSubplugin.AtomicfuKotlinGradleExtension::class.java).apply { + isJsIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION) + isJvmIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION) + } + } else { + // for KGP >= 1.6.20 && KGP <= 1.7.20: + // compiler plugin for JS IR is applied via the property `kotlinx.atomicfu.enableIrTransformation` + // compiler plugin for JVM IR is not supported yet + if (kotlinVersion.atLeast(1, 6, 20)) { + if (rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION_LEGACY)) { + plugins.apply(AtomicfuKotlinGradleSubplugin::class.java) } } } - withPluginWhenEvaluated("kotlin-multiplatform") { - configureMultiplatformPluginTasks() +} + +private fun Project.getBooleanProperty(name: String) = + rootProject.findProperty(name)?.toString()?.toBooleanStrict() ?: false + +private fun String.toBooleanStrict(): Boolean = when (this) { + "true" -> true + "false" -> false + else -> throw IllegalArgumentException("The string doesn't represent a boolean value: $this") +} + +private fun Project.needsJsIrTransformation(target: KotlinTarget): Boolean = + (rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION) || rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION_LEGACY)) + && target.isJsIrTarget() + +private fun KotlinTarget.isJsIrTarget() = (this is KotlinJsTarget && this.irTarget != null) || this is KotlinJsIrTarget + +private fun Project.addCompilerPluginDependency() { + if (isCompilerPluginAvailable()) { + withKotlinTargets { target -> + if (needsJsIrTransformation(target)) { + target.compilations.forEach { kotlinCompilation -> + kotlinCompilation.dependencies { + if (getKotlinVersion().atLeast(1, 7, 10)) { + // since Kotlin 1.7.10 we can add `atomicfu-runtime` dependency directly + implementation("org.jetbrains.kotlin:kotlinx-atomicfu-runtime:${getKotlinPluginVersion()}") + } else { + // add atomicfu compiler plugin dependency + // to provide the `atomicfu-runtime` library used during compiler plugin transformation + implementation("org.jetbrains.kotlin:atomicfu:${getKotlinPluginVersion()}") + } + } + } + } + } } } @@ -135,88 +217,112 @@ fun Project.withPluginWhenEvaluatedDependencies(plugin: String, fn: Project.(ver } fun Project.withKotlinTargets(fn: (KotlinTarget) -> Unit) { - extensions.findByType(KotlinProjectExtension::class.java)?.let { kotlinExtension -> - val targetsExtension = (kotlinExtension as? ExtensionAware)?.extensions?.findByName("targets") - @Suppress("UNCHECKED_CAST") - val targets = targetsExtension as NamedDomainObjectContainer<KotlinTarget> + extensions.findByType(KotlinTargetsContainer::class.java)?.let { kotlinExtension -> // find all compilations given sourceSet belongs to - targets.all { target -> fn(target) } + kotlinExtension.targets + .all { target -> fn(target) } } } -private fun KotlinCommonOptions.addFriendPaths(friendPathsFileCollection: FileCollection) { - val argName = when (this) { - is KotlinJvmOptions -> "-Xfriend-paths" - is KotlinJsOptions -> "-Xfriend-modules" - else -> return +private fun KotlinCompile<*>.setFriendPaths(friendPathsFileCollection: FileCollection) { + val (majorVersion, minorVersion) = project.getKotlinPluginVersion() + .split('.') + .take(2) + .map { it.toInt() } + if (majorVersion == 1 && minorVersion < 7) { + (this as? AbstractKotlinCompile<*>)?.friendPaths?.from(friendPathsFileCollection) + } else { + // See KT-KT-54167 (works only for KGP 1.7.0+) + (this as BaseKotlinCompile).friendPaths.from(friendPathsFileCollection) } - freeCompilerArgs = freeCompilerArgs + "$argName=${friendPathsFileCollection.joinToString(",")}" } -fun Project.configureMultiplatformPluginTasks() { - val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>() - val config = config +fun Project.configureJsTransformation() = + configureTransformationForTarget((kotlinExtension as KotlinJsProjectExtension).js()) + +fun Project.configureMultiplatformTransformation() = withKotlinTargets { target -> if (target.platformType == KotlinPlatformType.common || target.platformType == KotlinPlatformType.native) { return@withKotlinTargets // skip the common & native targets -- no transformation for them } - target.compilations.all compilations@{ compilation -> - val compilationType = compilation.name.compilationNameToType() - ?: return@compilations // skip unknown compilations - val classesDirs = compilation.output.classesDirs - // make copy of original classes directory - val originalClassesDirs: FileCollection = - project.files(classesDirs.from.toTypedArray()).filter { it.exists() } - originalDirsByCompilation[compilation] = originalClassesDirs - val transformedClassesDir = - project.buildDir.resolve("classes/atomicfu/${target.name}/${compilation.name}") - val transformTask = when (target.platformType) { - KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> { - if (!config.transformJvm) return@compilations // skip when transformation is turned off - project.createJvmTransformTask(compilation).configureJvmTask( - compilation.compileDependencyFiles, - compilation.compileAllTaskName, - transformedClassesDir, - originalClassesDirs, - config - ) - } - KotlinPlatformType.js -> { - if (!config.transformJs) return@compilations // skip when transformation is turned off - project.createJsTransformTask(compilation).configureJsTask( - compilation.compileAllTaskName, - transformedClassesDir, - originalClassesDirs, - config - ) - } - else -> error("Unsupported transformation platform '${target.platformType}'") - } - //now transformTask is responsible for compiling this source set into the classes directory - classesDirs.setFrom(transformedClassesDir) - classesDirs.builtBy(transformTask) - (tasks.findByName(target.artifactsTaskName) as? Jar)?.apply { - setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH) + configureTransformationForTarget(target) + } + +private fun Project.configureTransformationForTarget(target: KotlinTarget) { + val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>() + val config = config + target.compilations.all compilations@{ compilation -> + val compilationType = compilation.name.compilationNameToType() + ?: return@compilations // skip unknown compilations + val classesDirs = compilation.output.classesDirs + // make copy of original classes directory + val originalClassesDirs: FileCollection = + project.files(classesDirs.from.toTypedArray()).filter { it.exists() } + originalDirsByCompilation[compilation] = originalClassesDirs + val transformedClassesDir = + project.buildDir.resolve("classes/atomicfu/${target.name}/${compilation.name}") + val transformTask = when (target.platformType) { + KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> { + // skip transformation task if transformation is turned off or ir transformation is enabled + if (!config.transformJvm || rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) return@compilations + project.createJvmTransformTask(compilation).configureJvmTask( + compilation.compileDependencyFiles, + compilation.compileAllTaskName, + transformedClassesDir, + originalClassesDirs, + config + ) } - // test should compile and run against original production binaries - if (compilationType == CompilationType.TEST) { - val mainCompilation = - compilation.target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) - val originalMainClassesDirs = project.files( - // use Callable because there is no guarantee that main is configured before test - Callable { originalDirsByCompilation[mainCompilation]!! } + KotlinPlatformType.js -> { + // skip when js transformation is not needed or when IR is transformed + if (!config.transformJs || (needsJsIrTransformation(target))) { + return@compilations + } + project.createJsTransformTask(compilation).configureJsTask( + compilation.compileAllTaskName, + transformedClassesDir, + originalClassesDirs, + config ) + } + else -> error("Unsupported transformation platform '${target.platformType}'") + } + //now transformTask is responsible for compiling this source set into the classes directory + classesDirs.setFrom(transformedClassesDir) + classesDirs.builtBy(transformTask) + (tasks.findByName(target.artifactsTaskName) as? Jar)?.apply { + setupJarManifest(multiRelease = config.jvmVariant.toJvmVariant() == JvmVariant.BOTH) + } + // test should compile and run against original production binaries + if (compilationType == CompilationType.TEST) { + val mainCompilation = + compilation.target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + val originalMainClassesDirs = project.files( + // use Callable because there is no guarantee that main is configured before test + Callable { originalDirsByCompilation[mainCompilation]!! } + ) + // KGP >= 1.7.0 has breaking changes in task hierarchy: + // https://youtrack.jetbrains.com/issue/KT-32805#focus=Comments-27-5915479.0-0 + val (majorVersion, minorVersion) = getKotlinPluginVersion() + .split('.') + .take(2) + .map { it.toInt() } + if (majorVersion == 1 && minorVersion < 7) { (tasks.findByName(compilation.compileKotlinTaskName) as? AbstractCompile)?.classpath = originalMainClassesDirs + compilation.compileDependencyFiles - mainCompilation.output.classesDirs + } else { + (tasks.findByName(compilation.compileKotlinTaskName) as? AbstractKotlinCompileTool<*>) + ?.libraries + ?.setFrom( + originalMainClassesDirs + compilation.compileDependencyFiles - mainCompilation.output.classesDirs + ) + } - (tasks.findByName("${target.name}${compilation.name.capitalize()}") as? Test)?.classpath = - originalMainClassesDirs + (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyFiles - mainCompilation.output.classesDirs + (tasks.findByName("${target.name}${compilation.name.capitalize()}") as? Test)?.classpath = + originalMainClassesDirs + (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyFiles - mainCompilation.output.classesDirs - compilation.compileKotlinTask.doFirst { - compilation.kotlinOptions.addFriendPaths(originalMainClassesDirs) - } - } + compilation.compileKotlinTask.setFriendPaths(originalMainClassesDirs) } } } @@ -234,15 +340,16 @@ fun Project.sourceSetsByCompilation(): Map<KotlinSourceSet, List<KotlinCompilati } fun Project.configureMultiplatformPluginDependencies(version: String) { - if (rootProject.findProperty("kotlin.mpp.enableGranularSourceSetsMetadata").toString().toBoolean()) { + if (rootProject.getBooleanProperty("kotlin.mpp.enableGranularSourceSetsMetadata")) { + addCompilerPluginDependency() val mainConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets - .getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) - .compileOnlyConfigurationName + .getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) + .compileOnlyConfigurationName dependencies.add(mainConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version)) val testConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets - .getByName(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) - .implementationConfigurationName + .getByName(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) + .implementationConfigurationName dependencies.add(testConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version)) // For each source set that is only used in Native compilations, add an implementation dependency so that it @@ -258,20 +365,21 @@ fun Project.configureMultiplatformPluginDependencies(version: String) { } } else { sourceSetsByCompilation().forEach { (sourceSet, compilations) -> + addCompilerPluginDependency() val platformTypes = compilations.map { it.platformType }.toSet() val compilationNames = compilations.map { it.compilationName }.toSet() if (compilationNames.size != 1) error("Source set '${sourceSet.name}' of project '$name' is part of several compilations $compilationNames") val compilationType = compilationNames.single().compilationNameToType() - ?: return@forEach // skip unknown compilations + ?: return@forEach // skip unknown compilations val platform = - if (platformTypes.size > 1) Platform.MULTIPLATFORM else // mix of platform types -> "common" - when (platformTypes.single()) { - KotlinPlatformType.common -> Platform.MULTIPLATFORM - KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> Platform.JVM - KotlinPlatformType.js -> Platform.JS - KotlinPlatformType.native -> Platform.NATIVE - } + if (platformTypes.size > 1) Platform.MULTIPLATFORM else // mix of platform types -> "common" + when (platformTypes.single()) { + KotlinPlatformType.common -> Platform.MULTIPLATFORM + KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> Platform.JVM + KotlinPlatformType.js -> Platform.JS + KotlinPlatformType.native, KotlinPlatformType.wasm -> Platform.NATIVE + } val configurationName = when { // impl dependency for native (there is no transformation) platform == Platform.NATIVE -> sourceSet.implementationConfigurationName @@ -285,7 +393,7 @@ fun Project.configureMultiplatformPluginDependencies(version: String) { } } -fun Project.configureTransformTasks( +fun Project.configureJvmTransformation( testTaskName: String, createTransformTask: (sourceSet: SourceSet, transformedDir: File, originalDir: FileCollection) -> Task ) { @@ -305,7 +413,7 @@ fun Project.configureTransformTasks( //now transformTask is responsible for compiling this source set into the classes directory sourceSet.compiledBy(transformTask) (tasks.findByName(sourceSet.jarTaskName) as? Jar)?.apply { - setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH) + setupJarManifest(multiRelease = config.jvmVariant.toJvmVariant() == JvmVariant.BOTH) } // test should compile and run against original production binaries if (compilationType == CompilationType.TEST) { @@ -319,9 +427,7 @@ fun Project.configureTransformTasks( classpath = originalMainClassesDirs + sourceSet.compileClasspath - mainSourceSet.output.classesDirs - (this as? KotlinCompile<*>)?.doFirst { - kotlinOptions.addFriendPaths(originalMainClassesDirs) - } + (this as? KotlinCompile<*>)?.setFriendPaths(originalMainClassesDirs) } // todo: fix test runtime classpath for JS? @@ -331,7 +437,7 @@ fun Project.configureTransformTasks( } } -fun String.toVariant(): Variant = enumValueOf(toUpperCase(Locale.US)) +fun String.toJvmVariant(): JvmVariant = enumValueOf(toUpperCase(Locale.US)) fun Project.createJvmTransformTask(compilation: KotlinCompilation<*>): AtomicFUTransformTask = tasks.create( @@ -348,9 +454,6 @@ fun Project.createJsTransformTask(compilation: KotlinCompilation<*>): AtomicFUTr fun Project.createJvmTransformTask(sourceSet: SourceSet): AtomicFUTransformTask = tasks.create(sourceSet.getTaskName("transform", "atomicfuClasses"), AtomicFUTransformTask::class.java) -fun Project.createJsTransformTask(sourceSet: SourceSet): AtomicFUTransformJsTask = - tasks.create(sourceSet.getTaskName("transform", "atomicfuJsFiles"), AtomicFUTransformJsTask::class.java) - fun AtomicFUTransformTask.configureJvmTask( classpath: FileCollection, classesTaskName: String, @@ -363,7 +466,7 @@ fun AtomicFUTransformTask.configureJvmTask( classPath = classpath inputFiles = originalClassesDir outputDir = transformedClassesDir - variant = config.variant + jvmVariant = config.jvmVariant verbose = config.verbose } @@ -395,7 +498,7 @@ class AtomicFUPluginExtension(pluginVersion: String?) { var dependenciesVersion = pluginVersion var transformJvm = true var transformJs = true - var variant: String = "FU" + var jvmVariant: String = "FU" var verbose: Boolean = false } @@ -413,7 +516,8 @@ open class AtomicFUTransformTask : ConventionTask() { lateinit var classPath: FileCollection @Input - var variant = "FU" + var jvmVariant = "FU" + @Input var verbose = false @@ -422,7 +526,7 @@ open class AtomicFUTransformTask : ConventionTask() { val cp = classPath.files.map { it.absolutePath } inputFiles.files.forEach { inputDir -> AtomicFUTransformer(cp, inputDir, outputDir).let { t -> - t.variant = variant.toVariant() + t.jvmVariant = jvmVariant.toJvmVariant() t.verbose = verbose t.transform() } @@ -438,6 +542,7 @@ open class AtomicFUTransformJsTask : ConventionTask() { @OutputDirectory lateinit var outputDir: File + @Input var verbose = false |