diff --git a/build.sbt b/build.sbt index 3f93c647..f43b619a 100644 --- a/build.sbt +++ b/build.sbt @@ -93,27 +93,14 @@ lazy val gradlePlugin = project .in(file("scip-gradle-plugin")) .settings( name := "scip-gradle", - buildInfoPackage := "com.sourcegraph.scip_java", + javaOnlySettings, publish / skip := true, - scalacOptions ++= Seq("-target:11", "-release", "11"), libraryDependencies ++= List( "dev.gradleplugins" % "gradle-api" % V.gradle % Provided, - "dev.gradleplugins" % "gradle-test-kit" % V.gradle % Provided, - "org.jetbrains.kotlin" % "kotlin-gradle-plugin" % V.kotlinVersion % - Provided - ), - buildInfoKeys := - Seq[BuildInfoKey]( - version, - sbtVersion, - scalaVersion, - "javacModuleOptions" -> javacModuleOptions, - "scalametaVersion" -> V.scalameta, - "scala213" -> V.scala213 + "dev.gradleplugins" % "gradle-test-kit" % V.gradle % Provided ) ) - .enablePlugins(BuildInfoPlugin) lazy val javacPlugin = project .in(file("scip-javac")) diff --git a/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/ScipGradlePlugin.java b/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/ScipGradlePlugin.java new file mode 100644 index 00000000..8d264bb1 --- /dev/null +++ b/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/ScipGradlePlugin.java @@ -0,0 +1,187 @@ +package com.sourcegraph.gradle.scip; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.tasks.compile.JavaCompile; + +public class ScipGradlePlugin implements Plugin { + + @Override + public void apply(Project project) { + project.afterEvaluate(this::configureProject); + } + + private void configureProject(Project project) { + // Inject Maven Central/local so the indexer (and plugins like protobuf that + // resolve their own artifacts) can resolve dependencies even when the build + // being indexed doesn't declare any repositories of its own. + project.getRepositories().add(project.getRepositories().mavenCentral()); + project.getRepositories().add(project.getRepositories().mavenLocal()); + + Map extraProperties = + project.getExtensions().getExtraProperties().getProperties(); + + String targetRoot = requiredExtra(extraProperties, "scipTarget").toString(); + String sourceRoot = project.getRootDir().toString(); + + // Compilation tasks we need to trigger to index all the sources we care + // about. Built up as we detect the java and kotlin plugins. + List triggers = new ArrayList<>(); + + if (project.getPlugins().hasPlugin("java")) { + triggers.add("compileJava"); + triggers.add("compileTestJava"); + + boolean hasAnnotationPath; + try { + Configuration apConfig = project.getConfigurations().getByName("annotationProcessor"); + hasAnnotationPath = apConfig.isCanBeResolved() && !apConfig.getDependencies().isEmpty(); + } catch (Exception exc) { + hasAnnotationPath = false; + } + + Object javacPluginDep = project.files(requiredExtra(extraProperties, "javacPluginJar")); + boolean pluginAdded = tryAddJavacPlugin(project, javacPluginDep, hasAnnotationPath); + + project + .getTasks() + .withType(JavaCompile.class) + .configureEach( + task -> { + // Disable incremental compilation so the random timestamp added + // below forces a full recompile and Gradle doesn't cache stale + // SCIP state. + task.getOptions().setIncremental(false); + + if (pluginAdded) { + List args = task.getOptions().getCompilerArgs(); + + // It's important we don't add the plugin configuration more + // than once, as javac considers that an error. + boolean alreadyAdded = + args.stream().anyMatch(arg -> arg.startsWith("-Xplugin:scip")); + if (!alreadyAdded) { + // The random timestamp ensures the sources are _always_ + // recompiled, so Gradle doesn't cache any state. + // TODO: before this plugin is published to Maven Central, we + // will need to revert this change - as it can have + // detrimental effect on people's builds. + args.add( + "-Xplugin:scip -targetroot:" + + targetRoot + + " -sourceroot:" + + sourceRoot + + " -randomtimestamp=" + + System.nanoTime()); + } + } + }); + } + + boolean isKotlinMultiplatform = false; + for (Plugin plugin : project.getPlugins()) { + if (plugin.getClass().getName().contains("KotlinMultiplatform")) { + isKotlinMultiplatform = true; + break; + } + } + + if (project.getPlugins().hasPlugin("kotlin") || isKotlinMultiplatform) { + if (isKotlinMultiplatform) { + triggers.add("compileKotlinJvm"); + triggers.add("compileTestKotlinJvm"); + } else { + triggers.add("compileKotlin"); + triggers.add("compileTestKotlin"); + } + + // The CLI's init script provides the path of the embedded scip-kotlinc jar. + Object scipKotlinc = requiredExtra(extraProperties, "scipKotlincJar"); + project + .getTasks() + .configureEach( + task -> configureKotlinCompileTask(task, scipKotlinc, sourceRoot, targetRoot)); + } + + project.getTasks().create("scipCompileAll").dependsOn(triggers); + + project.getTasks().create("scipPrintDependencies", WriteDependencies.class); + } + + private static boolean tryAddJavacPlugin( + Project project, Object javacPluginDep, boolean hasAnnotationPath) { + try { + project.getDependencies().add("compileOnly", javacPluginDep); + if (hasAnnotationPath) { + project.getDependencies().add("annotationProcessor", javacPluginDep); + } + project.getDependencies().add("testCompileOnly", javacPluginDep); + return true; + } catch (Exception exc) { + // The `compileOnly` configuration has likely already been resolved by + // another plugin or buildscript, so we can no longer add new dependencies + // to it. The project will be skipped (no SCIP output) and the post-build + // check in `GradleBuildTool` will surface a clearer error. + project + .getLogger() + .warn( + "scip-java: failed to attach SCIP compiler plugin to project '" + + project.getName() + + "' (" + + exc.getClass().getSimpleName() + + ": " + + exc.getMessage() + + "). This subproject will not be indexed."); + return false; + } + } + + private static void configureKotlinCompileTask( + Task task, Object scipKotlinc, String sourceRoot, String targetRoot) { + if (!task.getClass().getSimpleName().contains("KotlinCompile")) { + return; + } + + // Referring to KotlinCompile directly here triggers NoClassDefFoundError - + // the plugin classpath is murky and we deliberately don't bundle the Kotlin + // Gradle plugin. So we commit the sins of reflection for our limited needs. + try { + Object kotlinOptions = task.getClass().getMethod("getKotlinOptions").invoke(task); + + @SuppressWarnings("unchecked") + List freeCompilerArgs = + (List) + kotlinOptions.getClass().getMethod("getFreeCompilerArgs").invoke(kotlinOptions); + + List newArgs = new ArrayList<>(freeCompilerArgs.size() + 5); + newArgs.addAll(freeCompilerArgs); + newArgs.add("-Xplugin=" + scipKotlinc); + newArgs.add("-P"); + newArgs.add("plugin:scip-kotlinc:sourceroot=" + sourceRoot); + newArgs.add("-P"); + newArgs.add("plugin:scip-kotlinc:targetroot=" + targetRoot); + + kotlinOptions + .getClass() + .getMethod("setFreeCompilerArgs", List.class) + .invoke(kotlinOptions, newArgs); + } catch (ReflectiveOperationException exc) { + throw new RuntimeException( + "scip-java: failed to configure Kotlin compile task '" + task.getName() + "'", exc); + } + } + + private static Object requiredExtra(Map extraProperties, String name) { + Object value = extraProperties.get(name); + if (value == null) { + throw new IllegalStateException( + name + " extra property must be set by the scip-java Gradle init script"); + } + return value; + } +} diff --git a/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/WriteDependencies.java b/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/WriteDependencies.java new file mode 100644 index 00000000..8f121a5e --- /dev/null +++ b/scip-gradle-plugin/src/main/java/com/sourcegraph/gradle/scip/WriteDependencies.java @@ -0,0 +1,141 @@ +package com.sourcegraph.gradle.scip; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.LinkedHashSet; +import java.util.Set; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.publish.PublishingExtension; +import org.gradle.api.publish.maven.MavenPublication; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; + +public class WriteDependencies extends DefaultTask { + + private static final String CROSS_REPO_BANNER = + "This will not prevent a SCIP index from being created, but the symbols\n" + + "extracted from this project won't be available for cross-repository navigation,\n" + + "as this project doesn't define any Maven coordinates by which it can be referred back" + + " to.\n" + + "See here for more details:" + + " https://sourcegraph.github.io/scip-java/docs/manual-configuration.html#step-5-optional-enable-cross-repository-navigation"; + + @TaskAction + public void printResolvedDependencies() throws IOException { + Project project = getProject(); + + // Always set by the scip-java Gradle init script. + Path depsOut = + Paths.get(project.getExtensions().getExtraProperties().get("dependenciesOut").toString()); + Files.createDirectories(depsOut.getParent()); + + String projectName = project.getName(); + String projectPath = project.getPath().replaceAll("[^a-z0-9A-Z_-]", "_"); + + // Write to a per-project file so multi-module builds don't collide or + // corrupt each other via parallel appends. The CLI globs every file whose + // name ends with "dependencies.txt". + Path dependenciesPath = + depsOut.getFileName().toString().endsWith("dependencies.txt") + ? depsOut.resolveSibling(projectPath + "." + depsOut.getFileName()) + : depsOut; + + // LinkedHashSet keeps first-seen order while dropping duplicates. + Set deps = new LinkedHashSet<>(); + + // List the project itself as a dependency so we can assign its Maven + // coordinates to the symbols it defines. + try { + PublishingExtension publishing = + project.getExtensions().findByType(PublishingExtension.class); + if (publishing != null) { + for (MavenPublication publication : + publishing.getPublications().withType(MavenPublication.class)) { + try { + SourceSet main = + project.getExtensions().getByType(SourceSetContainer.class).getByName("main"); + main.getOutput().getClassesDirs().getFiles().stream() + .map(File::getAbsolutePath) + .sorted() + .limit(1) + .forEach( + classesDirectory -> + deps.add( + String.join( + "\t", + publication.getGroupId(), + publication.getArtifactId(), + publication.getVersion(), + classesDirectory))); + } catch (Exception exception) { + String publicationName = + String.join( + ":", + publication.getGroupId(), + publication.getArtifactId(), + publication.getVersion()); + getLogger() + .warn( + "Failed to extract `main` source set from publication `" + + publicationName + + "` in project `" + + projectName + + "`.\n" + + CROSS_REPO_BANNER + + "\nHere's the raw error message:\n \"" + + exception.getMessage() + + "\"\nContinuing without cross-repository support."); + } + } + } + } catch (Exception exception) { + getLogger() + .warn( + "Failed to extract Maven publication from the project `" + + projectName + + "`.\n" + + CROSS_REPO_BANNER + + "\nHere's the raw error message (" + + exception.getClass() + + "):\n \"" + + exception.getMessage() + + "\"\nContinuing without cross-repository support."); + } + + project + .getConfigurations() + .forEach( + conf -> { + if (conf.isCanBeResolved()) { + try { + conf.getResolvedConfiguration() + .getResolvedArtifacts() + .forEach( + artifact -> + deps.add( + String.join( + "\t", + artifact.getModuleVersion().getId().getGroup(), + artifact.getModuleVersion().getId().getName(), + artifact.getModuleVersion().getId().getVersion(), + artifact.getFile().getAbsolutePath()))); + } catch (Exception exc) { + getLogger() + .warn( + "Skipping configuration '" + + conf.getName() + + "' due to resolution failure: " + + exc.getMessage()); + } + } + }); + + Files.write(dependenciesPath, deps, StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } +} diff --git a/scip-gradle-plugin/src/main/scala/ScipGradlePlugin.scala b/scip-gradle-plugin/src/main/scala/ScipGradlePlugin.scala deleted file mode 100644 index f635abb2..00000000 --- a/scip-gradle-plugin/src/main/scala/ScipGradlePlugin.scala +++ /dev/null @@ -1,397 +0,0 @@ -package com.sourcegraph.gradle.scip - -import java.nio.file.Files -import java.nio.file.Paths -import java.{util => ju} - -import scala.jdk.CollectionConverters._ -import scala.util._ - -import com.sourcegraph.scip_java.BuildInfo -import org.gradle.api.DefaultTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.compile.JavaCompile - -class ScipGradlePlugin extends Plugin[Project] { - import Logging._ - - override def apply(project: Project): Unit = { - project.afterEvaluate { project => - project.getRepositories().add(project.getRepositories().mavenCentral()) - project.getRepositories().add(project.getRepositories().mavenLocal()) - - val extra = project.getExtensions().getExtraProperties() - val extraProperties = extra.getProperties().asScala - - val targetRoot = extra - .getProperties() - .asScala - .getOrElse("scipTarget", project.getBuildDir()) - - val javacPluginVersion = BuildInfo.version - - val javacPluginJar = extraProperties - .get("javacPluginJar") - .map(_.asInstanceOf[String]) - - val javacPluginDep = javacPluginJar - .map[Object](jar => project.files(jar)) - // we fallback to javac plugin published to maven if there is no jar specified - // the JAR would usually be provided by auto-indexer - .getOrElse(s"com.sourcegraph:scip-javac:${javacPluginVersion}") - - val sourceRoot = project.getRootDir() - - val tasks = project.getTasks() - - // List of compilation commands that we will need to trigger - // to index all the sources in the project we care about. - // This list is built progressively as we check for java and kotlin - // plugins - val triggers = List.newBuilder[String] - - if (project.getPlugins().hasPlugin("java")) { - - triggers += "compileJava" - triggers += "compileTestJava" - - val hasAnnotationPath = Try( - project.getConfigurations().getByName("annotationProcessor") - ).map(apConfig => - if (apConfig.isCanBeResolved()) { - apConfig.getDependencies().size() > 0 - } else - false - ) - .toOption - .contains(true) - - val compilerPluginAdded = - try { - project.getDependencies().add("compileOnly", javacPluginDep) - - if (hasAnnotationPath) { - project - .getDependencies() - .add("annotationProcessor", javacPluginDep) - } - - project.getDependencies().add("testCompileOnly", javacPluginDep) - - true - } catch { - case exc: Exception => - // The `compileOnly` configuration has likely already been - // resolved by another plugin or buildscript, so we can no longer - // add new dependencies to it. The project will be skipped (no - // SCIP output) and the post-build check in - // `GradleBuildTool` will surface a clearer error. - warn( - s"scip-java: failed to attach SCIP compiler plugin to project '${project - .getName()}' (${exc.getClass().getSimpleName()}: ${exc - .getMessage()}). This subproject will not be indexed." - ) - false - } - - project - .getTasks() - .withType(classOf[JavaCompile]) - .configureEach { task => - // Add --add-exports JVM args so our compiler plugin can access - // javac internals. Required on JDK 17+ (JEP 403), no-op on 11-16. - val forkOptions = task.getOptions().getForkOptions() - val jvmArgs = - BuildInfo.javacModuleOptions.map(_.stripPrefix("-J")).asJava - forkOptions.getJvmArgs() match { - case null => - forkOptions.setJvmArgs(jvmArgs) - case _ => - forkOptions.getJvmArgs().addAll(jvmArgs) - } - - task.getOptions().setFork(true) - task.getOptions().setIncremental(false) - - if (compilerPluginAdded) { - val args = task.getOptions().getCompilerArgs() - - // It's important we don't add the plugin configuration more than - // once, as javac considers that an error - if (!args.asScala.exists(_.startsWith("-Xplugin:scip"))) { - args.addAll( - List( - // We add this to ensure that the sources are _always_ - // recompiled, so that Gradle doesn't cache any state - // TODO: before this plugin is published to Maven Central, - // we will need to revert this change - as it can have detrimental - // effect on people's builds - s"-Xplugin:scip -targetroot:$targetRoot -sourceroot:$sourceRoot -randomtimestamp=${System - .nanoTime()}" - ).asJava - ) - } - } - } - } - - val isKotlinMultiplatform = project - .getPlugins() - .asScala - .exists(_.getClass().getName().contains("KotlinMultiplatform")) - - if (project.getPlugins().hasPlugin("kotlin") || isKotlinMultiplatform) { - if (isKotlinMultiplatform) { - triggers += "compileKotlinJvm" - triggers += "compileTestKotlinJvm" - } else { - triggers += "compileKotlin" - triggers += "compileTestKotlin" - } - - project - .getTasks - .configureEach { task => - if (task.getClass().getSimpleName().contains("KotlinCompile")) { - - // I we actually refer to KotlinCompile at _any_ point here, then - // plugin fails with NoClassDefFoundError - because the plugin - // classpath is murky - // - // We also don't want to bundle kotlin plugin with this one as it - // can cause all sorts of troubles). - // - // Instead, we commit the sins of reflection for our limited - // needs. - val compilerArgs = task - .asInstanceOf[{ - def getKotlinOptions(): { - def getFreeCompilerArgs(): ju.List[String] - def setFreeCompilerArgs(args: ju.List[String]): Unit - // def getLanguageVersion(): Any - } - }] - .getKotlinOptions() - - // The scip-kotlinc compiler plugin is now built and shipped - // together with the scip-java CLI. The CLI's init script writes - // the absolute path of the embedded jar into the - // `scipKotlincJar` extra property so we no longer need to - // resolve a separately-published artifact at apply-time. - val scipKotlinc = extraProperties - .get("scipKotlincJar") - .map(_.asInstanceOf[String]) - .getOrElse { - throw new IllegalStateException( - "scipKotlincJar extra property must be set by the " + - "scip-java init script when indexing Kotlin sources" - ) - } - - val newArgs = - new ju.ArrayList[String]( - compilerArgs.getFreeCompilerArgs().size + 5 - ) - newArgs.addAll(compilerArgs.getFreeCompilerArgs()) - newArgs.addAll( - List( - "-Xplugin=" + scipKotlinc, - "-P", - s"plugin:scip-kotlinc:sourceroot=$sourceRoot", - "-P", - s"plugin:scip-kotlinc:targetroot=$targetRoot" - ).asJava - ) - - compilerArgs.setFreeCompilerArgs(newArgs) - } - } - } - - tasks.create( - "scipCompileAll", - { task => - triggers - .result() - .foldLeft(task) { case (tsk, trig) => - tsk.dependsOn(tasks.getByName(trig)) - } - - } - ) - - tasks.create("scipPrintDependencies", classOf[WriteDependencies]) - - } - - } - -} - -class WriteDependencies extends DefaultTask { - import Logging._ - - @TaskAction - def printResolvedDependencies(): Unit = { - - val depsOut = Option( - getProject().getExtensions().getExtraProperties().get("dependenciesOut") - ).map(_.toString).map(Paths.get(_)) - - depsOut.foreach(path => - java.nio.file.Files.createDirectories(path.getParent()) - ) - - val deps = List.newBuilder[String] - val project = getProject() - val projectName = project.getName() - val projectPath = project.getPath().replaceAll("[^a-z0-9A-Z_-]", "_") - val dependenciesPath = depsOut.map { path => - val filename = path.getFileName() - if (filename.endsWith("dependencies.txt")) { - val last = projectPath + "." + path.getFileName().toString() - path.getParent().resolve(last) - } else - path - } - - // List the project itself as a dependency so that we can assign project name/version to symbols that are defined in this project. - // The code below is roughly equivalent to the following with Groovy: - // deps += "$publication.groupId $publication.artifactId $publication.version $sourceSets.main.output.classesDirectory" - - val crossRepoBanner = - """ - |This will not prevent a SCIP index from being created, but the symbols - |extracted from this project won't be available for cross-repository navigation, - |as this project doesn't define any Maven coordinates by which it can be referred back to. - |See here for more details: https://sourcegraph.github.io/scip-java/docs/manual-configuration.html#step-5-optional-enable-cross-repository-navigation - """ - - Try( - project - .getExtensions() - .findByType(classOf[PublishingExtension]) - .getPublications() - .withType(classOf[MavenPublication]) - .asScala - ) match { - case Failure(exception) => - warn( - s""" - |Failed to extract Maven publication from the project `$projectName`. - $crossRepoBanner - |Here's the raw error message (${exception.getClass()}): - | "${exception.getMessage()}" - |Continuing without cross-repository support. - """.stripMargin.trim() - ) - - case Success(publications) => - publications.foreach { publication => - Try( - project - .getExtensions() - .getByType(classOf[SourceSetContainer]) - .getByName("main") - ) match { - case Failure(exception) => - val publicationName = List( - publication.getGroupId(), - publication.getArtifactId(), - publication.getVersion() - ).mkString(":") - - warn( - s""" - |Failed to extract `main` source set from publication `${publicationName}` in project `$projectName``. - $crossRepoBanner - |Here's the raw error message: - | "${exception.getMessage()}" - |Continuing without cross-repository support. - """.stripMargin.trim() - ) - - case Success(value) => - value - .getOutput() - .getClassesDirs() - .getFiles() - .asScala - .toList - .map(_.getAbsolutePath()) - .sorted - .take(1) - .foreach { classesDirectory => - deps += - List( - publication.getGroupId(), - publication.getArtifactId(), - publication.getVersion(), - classesDirectory - ).mkString("\t") - } - - } - } - } - - project - .getConfigurations() - .forEach { conf => - if (conf.isCanBeResolved()) { - try { - val resolved = conf.getResolvedConfiguration() - resolved - .getResolvedArtifacts() - .forEach { artif => - deps += - List( - artif.getModuleVersion().getId().getGroup(), - artif.getModuleVersion().getId().getName(), - artif.getModuleVersion().getId().getVersion(), - artif.getFile().getAbsolutePath() - ).mkString("\t") - - } - } catch { - case exc: Exception => - println( - s"Skipping configuration '${conf - .getName()}' due to resolution failure: ${exc.getMessage()}" - ) - } - - } - } - - val dependencies = deps.result().distinct - - dependenciesPath match { - case None => - dependencies.foreach(println) - case Some(path) => - Files.write( - path, - dependencies.asJava, - java.nio.file.StandardOpenOption.APPEND, - java.nio.file.StandardOpenOption.CREATE - ) - } - } -} - -private object Logging { - def info(msg: Any*) = System - .err - .println(s"[INFO] [scip-java.gradle] ${msg.mkString(" ")}") - - def warn(msg: Any*) = System - .err - .println(s"[WARNING] [scip-java.gradle] ${msg.mkString(" ")}") - -}