Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Project> {

@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<String, Object> 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<String> 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<String> 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<String> freeCompilerArgs =
(List<String>)
kotlinOptions.getClass().getMethod("getFreeCompilerArgs").invoke(kotlinOptions);

List<String> 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<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
Loading
Loading