diff --git a/buildSrc/src/main/kotlin/instrument.kt b/buildSrc/src/main/kotlin/instrument.kt new file mode 100644 index 00000000000..31a5408f5c5 --- /dev/null +++ b/buildSrc/src/main/kotlin/instrument.kt @@ -0,0 +1,170 @@ +@file:Suppress("unused") // usages in build scripts are not tracked properly + +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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. + */ + +import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionTask +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.tasks.* +import org.gradle.api.tasks.compile.AbstractCompile +import org.gradle.kotlin.dsl.* +import java.io.File + +fun Project.configureInstrumentation() { + plugins.matching { it::class.java.canonicalName.startsWith("org.jetbrains.kotlin.gradle.plugin") }.all { + // When we change the output classes directory, Gradle will automatically configure + // the test compile tasks to use the instrumented classes. Normally this is fine, + // however, it causes problems for Kotlin projects: + + // The "internal" modifier can be used to restrict access to the same module. + // To make it possible to use internal methods from the main source set in test classes, + // the Kotlin Gradle plugin adds the original output directory of the Java task + // as "friendly directory" which makes it possible to access internal members + // of the main module. Also this directory should be available on classpath during compilation + + // This fails when we change the classes dir. The easiest fix is to prepend the + // classes from the "friendly directory" to the compile classpath. + val testCompile = tasks.findByName("compileTestKotlin") as AbstractCompile? + testCompile?.doFirst { + val mainSourceSet = the().sourceSets.getByName("main") + testCompile.classpath = (testCompile.classpath + - mainSourceSet.output.classesDirs + + files((mainSourceSet as ExtensionAware).extra.get("classesDirsCopy"))) + } + } + + afterEvaluate { + the().sourceSets.all { sourceSetParam -> + // This copy will ignore filters, but they are unlikely to be used. + val classesDirs = (sourceSetParam.output.classesDirs as ConfigurableFileCollection).from as Collection + + val classesDirsCopy = project.files(classesDirs.toTypedArray()).filter { it.exists() } + (sourceSetParam as ExtensionAware).extra.set("classesDirsCopy", classesDirsCopy) + + logger.info("Saving old sources dir for project ${project.name}") + val instrumentedClassesDir = File(project.buildDir, "classes/${sourceSetParam.name}-instrumented") + (sourceSetParam.output.classesDirs as ConfigurableFileCollection).setFrom(instrumentedClassesDir) + val instrumentTask = project.tasks.create(sourceSetParam.getTaskName("instrument", "classes"), IntelliJInstrumentCodeTask::class.java) + instrumentTask.apply { + dependsOn(sourceSetParam.classesTaskName).onlyIf { !classesDirsCopy.isEmpty } + sourceSet = sourceSetParam + originalClassesDirs = classesDirsCopy + output = instrumentedClassesDir + } + + instrumentTask.outputs.dir(instrumentedClassesDir) + // Ensure that our task is invoked when the source set is built + sourceSetParam.compiledBy(instrumentTask) + @Suppress("UNUSED_EXPRESSION") + true + } + } +} + +@CacheableTask +open class IntelliJInstrumentCodeTask : ConventionTask() { + companion object { + private const val FILTER_ANNOTATION_REGEXP_CLASS = "com.intellij.ant.ClassFilterAnnotationRegexp" + private const val LOADER_REF = "java2.loader" + } + + var sourceSet: SourceSet? = null + + @Input + var originalClassesDirs: FileCollection? = null + + @get:InputFiles + val sourceDirs: FileCollection + get() = project.files(sourceSet!!.allSource.srcDirs.filter { !sourceSet!!.resources.contains(it) && it.exists() }) + + @get:OutputDirectory + var output: File? = null + + @TaskAction + fun instrumentClasses() { + logger.info("input files are: ${originalClassesDirs?.joinToString("; ", transform = { "'${it.name}'${if (it.exists()) "" else " (does not exists)" }"})}") + copyOriginalClasses() + + val classpath = project.ideaSdkDeps("javac2.jar", "jdom.jar", "asm-all.jar", "jgoodies-forms.jar") + + ant.withGroovyBuilder { + "taskdef"("name" to "instrumentIdeaExtensions", + "classpath" to classpath.asPath, + "loaderref" to LOADER_REF, + "classname" to "com.intellij.ant.InstrumentIdeaExtensions") + } + + logger.info("Compiling forms and instrumenting code with nullability preconditions") + val instrumentNotNull = prepareNotNullInstrumenting(classpath) + instrumentCode(sourceDirs, instrumentNotNull) + } + + private fun copyOriginalClasses() { + project.copy { + from(originalClassesDirs) + into(output) + } + } + + private fun prepareNotNullInstrumenting(classpath: ConfigurableFileCollection): Boolean { + ant.withGroovyBuilder { + "typedef"("name" to "skip", + "classpath" to classpath.asPath, + "loaderref" to LOADER_REF, + "classname" to FILTER_ANNOTATION_REGEXP_CLASS) + } + return true + } + + private fun instrumentCode(srcDirs: FileCollection, instrumentNotNull: Boolean) { + val headlessOldValue = System.setProperty("java.awt.headless", "true") + + // Instrumentation needs to have access to sources of forms for inclusion + val depSourceDirectorySets = project.configurations["compile"].dependencies.withType(ProjectDependency::class.java) + .map { p -> p.dependencyProject.the().sourceSets.getByName("main").allSource.sourceDirectories } + val instrumentationClasspath = + depSourceDirectorySets.fold(sourceSet!!.compileClasspath) { acc, v -> acc + v }.asPath.also { + logger.info("Using following dependency source dirs: $it") + } + + logger.info("Running instrumentIdeaExtensions with srcdir=${srcDirs.asPath}}, destdir=$output and classpath=$instrumentationClasspath") + + ant.withGroovyBuilder { + "instrumentIdeaExtensions"("srcdir" to srcDirs.asPath, + "destdir" to output, + "classpath" to instrumentationClasspath, + "includeantruntime" to false, + "instrumentNotNull" to instrumentNotNull) { + if (instrumentNotNull) { + ant.withGroovyBuilder { + "skip"("pattern" to "kotlin/Metadata") + } + } + } + } + + if (headlessOldValue != null) { + System.setProperty("java.awt.headless", headlessOldValue) + } else { + System.clearProperty("java.awt.headless") + } + } +} \ No newline at end of file diff --git a/idea/build.gradle.kts b/idea/build.gradle.kts index e3de3a7c424..8a015254af2 100644 --- a/idea/build.gradle.kts +++ b/idea/build.gradle.kts @@ -109,4 +109,5 @@ projectTest { testsJar {} classesDirsArtifact() +configureInstrumentation() diff --git a/idea/idea-gradle/build.gradle.kts b/idea/idea-gradle/build.gradle.kts index 1fac863ce44..f0c4f5d6fc4 100644 --- a/idea/idea-gradle/build.gradle.kts +++ b/idea/idea-gradle/build.gradle.kts @@ -2,7 +2,7 @@ apply { plugin("kotlin") } dependencies { - compileOnly(ideaSdkDeps("openapi", "idea", "external-system-rt")) + compileOnly(ideaSdkDeps("openapi", "idea", "external-system-rt", "forms_rt")) compileOnly(ideaPluginDeps("gradle-tooling-api", "gradle", "gradle-base-services", plugin = "gradle")) compileOnly(ideaPluginDeps("Groovy", plugin = "Groovy")) @@ -55,4 +55,6 @@ testsJar() projectTest { workingDir = rootDir -} \ No newline at end of file +} + +configureInstrumentation()