project/Common.scala (247 lines of code) (raw):

import AttributedClasspathUtils.buildIntellijSdkSubsetAttributedClasspath import CompilationCache.compilationCacheSettings import org.jetbrains.sbtidea.Keys.* import org.jetbrains.sbtidea.packaging.PackagingKeys.* import sbt.Keys.* import sbt.Project.projectToRef import sbt.{Def, *} import java.nio.file.Path import kotlin.Keys.{kotlinRuntimeProvided, kotlinVersion, kotlincJvmTarget} import kotlin.KotlinPlugin object Common { final val JetBrains = "JetBrains" private val globalJavacOptionsCommon = Seq( "-Xlint:unchecked", "-Xlint:deprecation" ) private val globalScalacOptionsCommon = Seq( "-explaintypes", "-deprecation", "-unchecked", "-feature", "-Xlint:serial", "-Xlint:nullary-unit", "-Xfatal-warnings", "-language:existentials", "-Ytasty-reader", "-Wunused:nowarn" ) private val globalScala3ScalacOptionsCommon = Seq( "-deprecation", "-explain", "-feature", "-unchecked", "-Werror", "-Wunused:implicits,imports", "-Wconf:msg=Non local returns are no longer supported:s", // I like local returns!!! "-Wconf:msg=Alphanumeric method isInstance is not declared infix:s", // and everyone loves infix notation! "-Wconf:msg=method .* is eta-expanded even though .* does not have the @FunctionalInterface annotation:s", // bullshit warning ) // options for modules which classes can only be used in IDEA process (uses JRE 21) // NOTE: we rely on the fact that javac & scalac use the same compiler option name, // though strictly speaking they have different types (they represent settings for different compilers) private val globalIdeaProcessReleaseOptions: Seq[String] = Seq("--release", "21") val globalJavacOptions: Seq[String] = globalJavacOptionsCommon ++ globalIdeaProcessReleaseOptions val globalScalacOptions: Seq[String] = globalScalacOptionsCommon ++ globalIdeaProcessReleaseOptions val globalScala3ScalacOptions: Seq[String] = globalScala3ScalacOptionsCommon ++ globalIdeaProcessReleaseOptions // options for modules which classes can be used outside IDEA process with arbitrary JVM version, e.g.: // - in JPS process (JDK is calculated based on project & module JDK) // - in Compile server (by default used project JDK version, can be explicitly changed by user) private val globalExternalProcessReleaseOptions: Seq[String] = Seq("--release", "8") // JDK 21 warns when we compile Java sources using "--release 8" that targeting JDK 8 will not be possible in the // future. I also checked JDK 25, which is the next LTS, "--release 8" is still available with the same warning, // so we should be fine for now. private val suppressObsoleteSourceTarget8: String = "-Xlint:-options" val outOfIDEAProcessJavacOptions: Seq[String] = globalJavacOptionsCommon ++ globalExternalProcessReleaseOptions :+ suppressObsoleteSourceTarget8 val outOfIDEAProcessScalacOptions: Seq[String] = globalScalacOptionsCommon ++ globalExternalProcessReleaseOptions val outOfIDEAProcessScala3ScalacOptions: Seq[String] = globalScala3ScalacOptionsCommon ++ globalExternalProcessReleaseOptions val projectDirectoriesSettings: Seq[Setting[?]] = Seq( // production sources Compile / sourceDirectory := baseDirectory.value / "src", // we put all source files in <subproject_dir>/src Compile / unmanagedSourceDirectories := Seq((Compile / sourceDirectory).value), // test sources Test / sourceDirectory := baseDirectory.value / "test", // we put all test source files in <subproject_dir>/test Test / unmanagedSourceDirectories := Seq((Test / sourceDirectory).value), //NOTE: this almost duplicates the logic from sbt-idea-plugin (see org.jetbrains.sbtidea.Init) //but it uses `:=` instead of `+=` to remove standard resource directories, which intersect with source directories // production resources Compile / resourceDirectory := baseDirectory.value / "resources", Compile / unmanagedResourceDirectories := Seq((Compile / resourceDirectory).value), // test resources //Note: we don't mark "testdata" as "test resources", because test data files are not test resources. //Those directories don't contain files which that should be copied `target/scala-2.13/test-classes Test / resourceDirectory := baseDirectory.value / "testResources", Test / unmanagedResourceDirectories := Seq((Test / resourceDirectory).value) ) val NoSourceDirectories: Seq[Setting[?]] = { val settings = Seq( sourceDirectories := Seq.empty, managedSourceDirectories := Seq.empty, unmanagedSourceDirectories := Seq.empty ) inConfig(Compile)(settings) ++ inConfig(Test)(settings) } // Adds dependency on Kotlin plugin // NOTE: this val might not be used but is handy to keep here in case one needs to debug Kotlin code private lazy val AddKotlinPluginDependenciesSettings: Seq[Setting[?]] = Seq( intellijPlugins += "org.jetbrains.kotlin".toPlugin, // Kotlin plugin jars bundle some Kotlin Analysis Api classes, however, no sources are bundled in the IJ sources archive libraryDependencies ++= KotlinAnalysisApiIdeSourcesDependencies, ) private lazy val KotlinAnalysisApiIdeSourcesDependencies: Seq[ModuleID] = { // Unfortunately, we can't automatically detect this version. It's not published in any artifacts // NOTE: take the latest version from (Use proper branch) // https://github.com/JetBrains/intellij-community/blob/master/.idea/libraries/kotlinc_analysis_api.xml val KotlinAnalysisApiVersion = "2.2.20-ij252-17" Seq( "org.jetbrains.kotlin" % "analysis-api-fe10-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "analysis-api-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "analysis-api-impl-base-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "analysis-api-k2-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "analysis-api-platform-interface-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "analysis-api-standalone-for-ide" % KotlinAnalysisApiVersion, "org.jetbrains.kotlin" % "kotlin-compiler-common-for-ide" % KotlinAnalysisApiVersion, ).map(_ .withSources() // `withSources` instead of `sources` in order the library is visible in the project view .intransitive() // It seems each jar is self-contained, but the pom metadata contains dependencies, so we need to use "intransitive" to avoid failed download errors ) } private val NewProjectBaseSettings: Seq[Setting[?]] = Seq( organization := JetBrains, scalaVersion := Versions.scalaVersion, (Compile / javacOptions) := globalJavacOptions, (Compile / scalacOptions) := globalScalacOptions, updateOptions := updateOptions.value.withCachedResolution(true), instrumentThreadingAnnotations := true, libraryDependencies ++= Seq( //jetbrains annotations library is quite minimalistic, it's required for @Nullable/@NotNull/@Nls/etc.. annotations Dependencies.jetbrainsAnnotations % Provided, Dependencies.junit % Test, Dependencies.junitParams % Test, Dependencies.junitInterface % Test, Dependencies.opentest4j % Test ), ) ++ projectDirectoriesSettings ++ compilationCacheSettings val intellijPluginsScopeFilter: ScopeFilter = ScopeFilter(inDependencies(ThisProject, includeRoot = false)) //Common settings for Community & Ultimate main projects val MainProjectSettings: Seq[Setting[?]] = Seq( sourcesInBase := false, packageMethod := PackagingMethod.Standalone(), libraryDependencies ++= Seq( Dependencies.scalaLibrary, Dependencies.scala3Library, //Original commit message: //scala-reflect.jar could be excluded from package mappings in scala-impl, because jars from scala-lang are not // package by default in non-root modules. It was non-deterministic and happened on my machine, but not on buildserver. //https://github.com/JetBrains/sbt-idea-plugin/blob/d7d8a421cc4ff10ea723ce116a79cb4491d7e38d/packaging/src/main/scala/org/jetbrains/sbtidea/packaging/PackagingKeysInit.scala#L26 Dependencies.scalaReflect, Dependencies.scalaXml ), packageLibraryMappings := Seq( Dependencies.scalaLibrary -> Some("lib/scala-library.jar"), Dependencies.scala3Library -> Some("lib/scala3-library_3.jar"), Dependencies.scalaReflect -> Some("lib/scala-reflect.jar"), Dependencies.scalaXml -> Some("lib/scala-xml.jar"), ), intellijPlugins := intellijPlugins.all(intellijPluginsScopeFilter).value.flatten.distinct, intellijExtraRuntimePluginsInTests := Seq( //Below are some other useful plugins which you might be interested to inspect //We don't have any dependencies on those plugins, however sometimes it might be useful to see how some features are implemented in them plugin. //You can uncomment any of them locally //This bundled plugin contains some internal development tools such as "View Psi Structure" action //(note there is also PsiViewer plugin, but it's a different plugin) //"com.intellij.dev".toPlugin, "org.jetbrains.kotlin".toPlugin ), Test / javaOptions += "-Didea.log.leaked.projects.in.tests=false" ) def newPlainScalaProject(projectName: String, base: File): Project = Project(projectName, base).settings( NewProjectBaseSettings ).settings( name := projectName, intellijMainJars := Seq.empty, intellijTestJars := Seq.empty, intellijPlugins := Seq.empty, ) def newProject(projectName: String, base: File): Project = Project(projectName, base).settings( NewProjectBaseSettings ).settings( name := projectName, intellijMainJars ~= { _.filterNot(Dependencies.excludeJarsFromPlatformDependencies).filter(_.exists()) }, intellijPlugins += "com.intellij.java".toPlugin, pathExcludeFilter := excludePathsFromPackage _ ) /** * ATTENTION: Kotlin modules should be used only in those cases when it is impossible or very hard to extend * platform functionality in Scala (due to the inherent requirements of the platform and only for the interop) */ def newProjectWithKotlin(projectName: String): Project = newProjectWithKotlin(projectName, file(projectName)) /** * ATTENTION: Kotlin modules should be used only in those cases when it is impossible or very hard to extend * platform functionality in Scala (due to the inherent requirements of the platform and only for the interop) */ def newProjectWithKotlin(projectName: String, base: File): Project = newProject(projectName, base) .enablePlugins(KotlinPlugin) .settings( // NOTE: check community/.idea/libraries/kotlin_stdlib.xml in intellij monorepo when updating intellijVersion // NOTE: keep versions in sync with ultimate/.idea/kotlinc.xml and community/.idea/kotlinc.xml kotlinVersion := "2.2.20", kotlincJvmTarget := "21", kotlinRuntimeProvided := true, resolvers += DependencyResolvers.IntelliJDependencies, ) implicit class ProjectOps(private val project: Project) extends AnyVal { /** * Manually build the classpath for the JPS module. * Code from JPS modules is executed in the JPS process, which has a separate classpath. * * @note this classpath is only required to properly compile the module * (in order we do not accidentally use any classes that are not available in the JPS process)<br> * At runtime the classpath will be constructed in by Platform. * @see [[IntellijSdkSubsetInfo.Jps]] */ def withJpsClasspath: Project = withIntellijSubsetDependency(IntellijSdkSubsetInfo.Jps) /** * Similar to [[withJpsClasspath]] but defines the classes that are used in both JPS and IntelliJ processes */ def withJpsSharedClasspath: Project = withIntellijSubsetDependency(IntellijSdkSubsetInfo.JpsShared) private def withIntellijSubsetDependency(subsetInfo: IntellijSdkSubsetInfo): Project = { project.settings( // This line only registers information about special INTELLIJ-SDK-* libraries in the update report update := UpdateWithIDEAInjectionTasks2.getUpdateReportWithIntellijSdkSubsetModuleTask(subsetInfo).value, // This line adds the special INTELLIJ-SDK-* module (~library) to the classpath project classpath Compile / externalDependencyClasspath ++= { val buildInfo = productInfo.value.buildNumber val intellijBaseDir = intellijBaseDirectory.value val info = subsetInfo.toMaterialisedInfo(buildInfo, intellijBaseDir) buildIntellijSdkSubsetAttributedClasspath(info, Compile) } ) } /** * @note Be careful when applying this to sbt subprojects. * Any `Compile / scalacOptions := Seq(...)` specified after this method is called will completely override * the scalac plugin, and it will not be applied. */ def withCompilerPluginIn(plugin: Project): Project = withCompilerPluginIn(projectToRef(plugin)) /** * @note Be careful when applying this to sbt subprojects. * Any `Compile / scalacOptions := Seq(...)` specified after this method is called will completely override * the scalac plugin, and it will not be applied. */ def withCompilerPluginIn(plugin: ProjectReference): Project = project .dependsOn( plugin % Provided ) .settings( // TODO Only Test / scalacOptions Compile / scalacOptions ++= Seq( s"-Xplugin:${(plugin / Compile / classDirectory).value}", s"-Xplugin-require:${(plugin / name).value}") ) def projectWithTestsOnly: Project = project.settings( Compile / sourceDirectories := Nil, Compile / resourceDirectories := Nil, Compile / unmanagedSourceDirectories := Nil, Compile / unmanagedResourceDirectories := Nil, // Packaging a module with tests only doesn't make sense packageMethod := PackagingMethod.Skip(), ) } private def excludePathsFromPackage(path: java.nio.file.Path): Boolean = `is signature file in META-INF`(path) //This filtering was originally added within SCL-14474 //TODO we should generally filter META-INF when merging jars private def `is signature file in META-INF`(path: Path): Boolean = { val parent = path.getParent val filename = path.getFileName.toString // exclude .../META-INF/*.RSA *.SF parent != null && parent.toString == "META-INF" && (filename.endsWith(".RSA") || filename.endsWith(".SF")) } def newProject(projectName: String): Project = newProject(projectName, file(projectName)) def deduplicatedClasspath(classpaths: Keys.Classpath*): Keys.Classpath = { val merged = classpaths.foldLeft(Seq.empty[Attributed[File]]) { (merged, cp) => merged ++ cp } merged.sortBy(_.data.getCanonicalPath).distinct } object TestCategory { private val pkg = "org.jetbrains.plugins.scala" private def cat(name: String) = s"$pkg.$name" val fileSetTests: String = cat("FileSetTests") val compilationTestsZinc: String = cat("CompilationTests_Zinc") val compilationTestsIDEA: String = cat("CompilationTests_IDEA") val compilerHighlightingTests: String = cat("CompilerHighlightingTests") val completionTests: String = cat("CompletionTests") val editorTests: String = cat("EditorTests") val slowTests: String = cat("SlowTests") val slowTests2: String = cat("SlowTests2") val debuggerTests: String = cat("DebuggerTests") val debuggerEvaluationTests: String = cat("DebuggerEvaluationTests") val scalacTests: String = cat("ScalacTests") val typecheckerTests: String = cat("TypecheckerTests") val testingSupportTests: String = cat("TestingSupportTests") val textToTextTests: String = cat("TextToTextTests") val worksheetEvaluationTests: String = cat("WorksheetEvaluationTests") val highlightingTests: String = cat("HighlightingTests") val randomTypingTests: String = cat("RandomTypingTests") val flakyTests: String = cat("FlakyTests") val bundleSortingTests: String = cat("BundleSortingTests") } def pluginVersion: String = Option(System.getProperty("plugin.version")).getOrElse("SNAPSHOT") def replaceInFile(f: File, source: String, target: String): Unit = { if (!(source == null) && !(target == null)) { IO.writeLines(f, IO.readLines(f).map(_.replace(source, target))) } } def patchPluginXML(f: File): File = { val tmpFile = java.io.File.createTempFile("plugin", ".xml") IO.copyFile(f, tmpFile) replaceInFile(tmpFile, "VERSION", pluginVersion) tmpFile } lazy val cleanAll: TaskKey[Unit] = taskKey("Cleans all modules") def cleanAllTask(includeBuild: Option[BuildRef]): Def.Initialize[Task[Unit]] = Def.taskDyn { val structure = buildStructure.value val build = thisProjectRef.value.build val projects = structure.allProjectRefs(build) ++ includeBuild.toSeq.flatMap(b => structure.allProjectRefs(b.build)) val scopeFilter = ScopeFilter(inProjects(projects *), inAnyConfiguration) Def.task { clean.all(scopeFilter).value } } }