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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.TaskState;
import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.process.CommandLineArgumentProvider;
Expand Down Expand Up @@ -137,11 +139,43 @@ private void configureProject(Project project) {
}
}

// Kotlin sources are not indexed inside kotlinc: after the Kotlin compile
// tasks, their sources and classpath are dumped for the scip-java CLI to
// index after the build, decoupling indexing from the build's Kotlin
// compiler version. The dump is a finalizer rather than an action of the
// compile tasks, because Gradle skips their actions when they are up to
// date or restored from the build cache, while the CLI deletes the
// targetroot before every build. The finalizer declares no outputs, so
// Gradle runs it every time.
List<Task> kotlinCompileTasks = new ArrayList<>();
TaskProvider<Task> kotlinConfigs =
project
.getTasks()
.register(
"scipKotlinConfigs",
export ->
export.doLast(
ignored -> {
for (Task task : kotlinCompileTasks) {
// Only tasks of this build that succeeded; up-to-date
// and cached tasks count as executed.
TaskState state = task.getState();
if (state.getExecuted() && state.getFailure() == null) {
writeKotlinConfig(task, sourceRoot, targetRoot);
}
}
}));
// Not limited to the plugin checks above: AGP 9's built-in Kotlin adds
// Kotlin compile tasks to Android modules without applying a Kotlin plugin.
project
.getTasks()
.configureEach(task -> configureKotlinCompileTask(task, sourceRoot, targetRoot));
.configureEach(
task -> {
if (task.getClass().getSimpleName().contains("KotlinCompile")) {
kotlinCompileTasks.add(task);
task.finalizedBy(kotlinConfigs);
}
});

project.getTasks().create("scipCompileAll").dependsOn(triggers);

Expand Down Expand Up @@ -237,71 +271,62 @@ private static Set<String> addJavacPlugin(Project project, Object javacPluginDep
}

/**
* Kotlin sources are not indexed inside kotlinc: after each Kotlin compile task runs, its sources
* and classpath are dumped to {@code <targetroot>/kotlin-configs/<task>.txt} for the scip-java
* CLI to index after the build, decoupling indexing from the build's Kotlin compiler version.
* Dumps the sources and classpath of a Kotlin compile task to {@code kotlin-configs/<task>.txt}.
*/
private static void configureKotlinCompileTask(Task task, String sourceRoot, String targetRoot) {
if (!task.getClass().getSimpleName().contains("KotlinCompile")) {
return;
}
private static void writeKotlinConfig(Task task, String sourceRoot, String targetRoot) {
try {
List<String> lines = new ArrayList<>();
lines.add("sourceroot " + sourceRoot);

task.doLast(
ignored -> {
try {
List<String> lines = new ArrayList<>();
lines.add("sourceroot " + sourceRoot);

// Referring to KotlinCompile directly triggers NoClassDefFoundError -
// the plugin classpath is murky and we deliberately don't bundle the
// Kotlin Gradle plugin. So we commit the sins of reflection.
FileCollection libraries;
try {
libraries = (FileCollection) task.getClass().getMethod("getLibraries").invoke(task);
} catch (ReflectiveOperationException exc) {
libraries = (FileCollection) task.getClass().getMethod("getClasspath").invoke(task);
}
for (File entry : libraries.getFiles()) {
lines.add("classpath " + entry.getAbsolutePath());
}

int sources = 0;
for (File file : task.getInputs().getSourceFiles().getFiles()) {
if (file.getName().endsWith(".kt") || file.getName().endsWith(".kts")) {
lines.add("source " + file.getAbsolutePath());
sources++;
}
}
// Java sources of the same module are listed too, so Kotlin
// references to them resolve. The indexer only emits Kotlin files.
try {
FileCollection javaSources =
(FileCollection) task.getClass().getMethod("getJavaSources").invoke(task);
for (File file : javaSources.getFiles()) {
if (file.getName().endsWith(".java")) {
lines.add("source " + file.getAbsolutePath());
}
}
} catch (ReflectiveOperationException exc) {
// Kotlin Gradle Plugin without javaSources (pre 1.7).
}
if (sources == 0) {
return;
}

Path configDir = Paths.get(targetRoot, "kotlin-configs");
Files.createDirectories(configDir);
String fileName = task.getPath().replaceAll("[^A-Za-z0-9]", "-") + ".txt";
Files.write(configDir.resolve(fileName), lines);
} catch (Exception exc) {
throw new RuntimeException(
"scip-java: failed to extract Kotlin compile configuration for task '"
+ task.getName()
+ "': "
+ exc,
exc);
// Referring to KotlinCompile directly triggers NoClassDefFoundError -
// the plugin classpath is murky and we deliberately don't bundle the
// Kotlin Gradle plugin. So we commit the sins of reflection.
FileCollection libraries;
try {
libraries = (FileCollection) task.getClass().getMethod("getLibraries").invoke(task);
} catch (ReflectiveOperationException exc) {
libraries = (FileCollection) task.getClass().getMethod("getClasspath").invoke(task);
}
for (File entry : libraries.getFiles()) {
lines.add("classpath " + entry.getAbsolutePath());
}

int sources = 0;
for (File file : task.getInputs().getSourceFiles().getFiles()) {
if (file.getName().endsWith(".kt") || file.getName().endsWith(".kts")) {
lines.add("source " + file.getAbsolutePath());
sources++;
}
}
// Java sources of the same module are listed too, so Kotlin
// references to them resolve. The indexer only emits Kotlin files.
try {
FileCollection javaSources =
(FileCollection) task.getClass().getMethod("getJavaSources").invoke(task);
for (File file : javaSources.getFiles()) {
if (file.getName().endsWith(".java")) {
lines.add("source " + file.getAbsolutePath());
}
});
}
} catch (ReflectiveOperationException exc) {
// Kotlin Gradle Plugin without javaSources (pre 1.7).
}
if (sources == 0) {
return;
}

Path configDir = Paths.get(targetRoot, "kotlin-configs");
Files.createDirectories(configDir);
String fileName = task.getPath().replaceAll("[^A-Za-z0-9]", "-") + ".txt";
Files.write(configDir.resolve(fileName), lines);
} catch (Exception exc) {
throw new RuntimeException(
"scip-java: failed to extract Kotlin compile configuration for task '"
+ task.getName()
+ "': "
+ exc,
exc);
}
}

private static Object requiredExtra(Map<String, Object> extraProperties, String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ This means our SCIP compiler plugin was not attached to one or more JavaCompile
cmd += script
cmd += "-Dscip.targetroot=${targetroot()}"
cmd += index.finalBuildCommand(listOf("clean", "scipPrintDependencies", "scipCompileAll"))
// The Gradle plugin isn't compatible with the configuration cache, which
// a build can enable in gradle.properties. Last, because the last of
// --configuration-cache and --no-configuration-cache wins.
cmd += "--no-configuration-cache"

targetroot().toFile().deleteRecursively()
val result = index.app.runProcess(cmd, env = mapOf("TERM" to "dumb"))
Expand Down
31 changes: 18 additions & 13 deletions scip-java/src/test/kotlin/tests/BuildToolHarness.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ abstract class BuildToolHarness {
targetRoot: String? = null,
maxJdk: Int? = null,
substitutions: Map<String, String> = emptyMap(),
runs: Int = 1,
): DynamicTest =
dynamicTest(name) {
// Some build tools cap the JDK they support; skip rather than fail
Expand Down Expand Up @@ -143,21 +144,25 @@ abstract class BuildToolHarness {
targetroot.toString(),
) + extraArguments

val (exit, output) = runScipJava(workingDirectory, arguments)
// Later runs reuse the checkout, so they cover state left by
// earlier builds (up-to-date tasks, build cache entries).
for (run in 1..runs) {
val (exit, output) = runScipJava(workingDirectory, arguments)

if (expectedError != null) {
assertNotEquals(0, exit, "expected failure, output:\n$output")
expectedError(output)
} else {
assertEquals(0, exit, "expected success, output:\n$output")
}
if (expectedError != null) {
assertNotEquals(0, exit, "expected failure, output:\n$output")
expectedError(output)
} else {
assertEquals(0, exit, "expected success, output:\n$output")
}

val shards = listScipShards(targetroot)
if (shards.size != expectedScipFiles) {
fail(
"Expected $expectedScipFiles SCIP shard(s), got ${shards.size}: " +
"$shards\noutput:\n$output"
)
val shards = listScipShards(targetroot)
if (shards.size != expectedScipFiles) {
fail(
"Run $run: expected $expectedScipFiles SCIP shard(s), got ${shards.size}: " +
"$shards\noutput:\n$output"
)
}
}

if (expectedPackages.isNotEmpty()) {
Expand Down
42 changes: 42 additions & 0 deletions scip-java/src/test/kotlin/tests/GradleBuildToolTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class GradleBuildToolTest : BuildToolHarness() {
expectedPackages: String = "",
extraArguments: List<String> = emptyList(),
substitutions: Map<String, String> = emptyMap(),
runs: Int = 1,
): DynamicTest =
checkBuild(
"$name-gradle-$GRADLE_VERSION",
Expand All @@ -28,6 +29,7 @@ class GradleBuildToolTest : BuildToolHarness() {
expectedPackages = expectedPackages,
extraArguments = extraArguments,
substitutions = substitutions,
runs = runs,
initCommand = { workingDirectory ->
// Gradle needs a build script present to generate the wrapper;
// the real one is laid down afterwards from the fixture.
Expand Down Expand Up @@ -161,7 +163,47 @@ class GradleBuildToolTest : BuildToolHarness() {
extraArguments = listOf("--", "compileJava"),
)
)
// Regression tests: the configuration cache is disabled, both when the
// build enables it and when the build command asks for it. Otherwise
// scipPrintDependencies fails, and a second run reuses the configuration
// cache entry of the first, which leaves compileJava up to date.
add(
checkGradleBuild(
"configuration-cache",
"gradle/configuration-cache",
expectedScipFiles = 1,
)
)
add(
checkGradleBuild(
"java-configuration-cache",
"gradle/build-command",
expectedScipFiles = 1,
extraArguments = listOf("--", "--configuration-cache", "compileJava"),
runs = 2,
)
)
add(checkGradleBuild("kotlin2", "gradle/kotlin2", expectedScipFiles = 4))
// Regression tests: Kotlin sources must be indexed again when the Kotlin
// compile tasks are restored from the build cache, or are up to date
// because the build command doesn't clean.
add(
checkGradleBuild(
"kotlin-build-cache",
"gradle/kotlin-build-cache",
expectedScipFiles = 4,
runs = 2,
)
)
add(
checkGradleBuild(
"kotlin-up-to-date",
"gradle/kotlin2",
expectedScipFiles = 2,
extraArguments = listOf("--", "compileKotlin", "compileJava"),
runs = 2,
)
)
add(
checkGradleBuild(
"implementation-deps",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
apply plugin: 'java'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.configuration-cache=true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public class Example {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.4.0'
}
kotlin {
jvmToolchain(17)
}
repositories {
mavenCentral()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.caching=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// A build cache inside the checkout, so every test starts with an empty
// cache and leaves the user's Gradle home untouched.
buildCache {
local {
directory = file('build-cache')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package foo;
public class JExample {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package foo
object Example {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package foo;
public class JExampleSuite {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package foo
class ExampleSuite {}
Loading