Provide incremental compilation for Maven

#KT-11916 fixed

To use the IC either:
1. set the `kotlin.compiler.incremental` property to `true` in a pom.xml:
```
<properties>
    <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
</properties>
```
2. pass the `kotlin.compiler.incremental` property in a command line:
```
mvn install -Dkotlin.compiler.incremental=true
```

When IC is on Kotlin plugin is expected to print the warning in the log:
```
Using experimental Kotlin incremental compilation
```

After each call an incremental compiler will also log how many files it has compiled:
```
Compiled %SOME_NUMBER% Kotlin files using incremental compiler
```

Note that the first build will be non-incremental.

For more diagnostic information (such as an exact list of compiled files) use the `kotlin.compiler.incremental.log.level` system property:
```
mvn install -Dkotlin.compiler.incremental=true -Dkotlin.compiler.incremental.log.level=info
```

To force the rebuild just run the 'clean' goal:
```
mvn clean install
```
This commit is contained in:
Alexey Tsvetkov
2017-03-30 16:25:31 +03:00
parent 21da11fe18
commit 30d6af7aae
14 changed files with 665 additions and 21 deletions
@@ -0,0 +1,5 @@
package org.jetbrains.kotlin.maven;
interface Action<T> {
void run(T param) throws Exception;
}
@@ -0,0 +1,61 @@
package org.jetbrains.kotlin.maven;
import org.junit.Test;
import java.io.File;
public class IncrementalCompilationIT {
@Test
public void testSimpleCompile() throws Exception {
MavenProject project = new MavenProject("kotlinSimple");
project.exec("package")
.succeeded()
.compiledKotlin("src/A.kt", "src/useA.kt", "src/Dummy.kt");
}
@Test
public void testNoChanges() throws Exception {
MavenProject project = new MavenProject("kotlinSimple");
project.exec("package");
project.exec("package")
.succeeded()
.compiledKotlin();
}
@Test
public void testCompileError() throws Exception {
MavenProject project = new MavenProject("kotlinSimple");
project.exec("package");
File aKt = project.file("src/A.kt");
String original = "class A";
String replacement = "private class A";
MavenTestUtils.replaceFirstInFile(aKt, original, replacement);
project.exec("package")
.failed()
.contains("Cannot access 'A': it is private in file");
MavenTestUtils.replaceFirstInFile(aKt, replacement, original);
project.exec("package")
.succeeded()
.compiledKotlin("src/A.kt", "src/useA.kt");
}
@Test
public void testFunctionVisibilityChanged() throws Exception {
MavenProject project = new MavenProject("kotlinSimple");
project.exec("package");
File aKt = project.file("src/A.kt");
MavenTestUtils.replaceFirstInFile(aKt, "fun foo", "internal fun foo");
project.exec("package")
.succeeded()
.compiledKotlin("src/A.kt", "src/useA.kt");
// todo rebuild and compare output
}
}
@@ -0,0 +1,113 @@
package org.jetbrains.kotlin.maven;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class MavenExecutionResult {
@NotNull
private final String stdout;
@NotNull
private final File workingDir;
private int exitCode;
MavenExecutionResult(
@NotNull String output,
@NotNull File workingDir,
int exitCode
) {
this.stdout = output;
this.workingDir = workingDir;
this.exitCode = exitCode;
}
MavenExecutionResult check(@NotNull Action<MavenExecutionResult> fn) throws Exception {
try {
fn.run(this);
}
catch (Throwable t) {
System.out.println(stdout);
throw new RuntimeException(t);
}
return this;
}
MavenExecutionResult succeeded() throws Exception {
return check(new Action<MavenExecutionResult>() {
@Override
public void run(MavenExecutionResult execResult) {
Assert.assertEquals("Maven process was expected to succeeded", 0, exitCode);
}
});
}
MavenExecutionResult failed() throws Exception {
return check(new Action<MavenExecutionResult>() {
@Override
public void run(MavenExecutionResult execResult) {
Assert.assertNotEquals("Maven process was expected to fail", 0, exitCode);
}
});
}
MavenExecutionResult contains(@NotNull final String str) throws Exception {
return check(new Action<MavenExecutionResult>() {
@Override
public void run(MavenExecutionResult execResult) {
if (!stdout.contains(str)) {
throw new AssertionError("Maven output should contain '" + str + "'");
}
}
});
}
MavenExecutionResult notContains(@NotNull final String str) throws Exception {
return check(new Action<MavenExecutionResult>() {
@Override
public void run(MavenExecutionResult execResult) {
if (stdout.contains(str)) {
throw new AssertionError("Maven output should not contain '" + str + "'");
}
}
});
}
MavenExecutionResult compiledKotlin(@NotNull final String... expectedPaths) throws Exception {
return check(new Action<MavenExecutionResult>() {
@Override
public void run(MavenExecutionResult execResult) {
Pattern kotlinCompileIteration = Pattern.compile("(?m)Kotlin compile iteration: (.*)$");
Matcher m = kotlinCompileIteration.matcher(stdout);
Set<String> normalizedActualPaths = new HashSet<String>();
while (m.find()) {
String[] compiledFiles = m.group(1).split(",");
for (String path : compiledFiles) {
File file = new File(path.trim());
String relativePath = FileUtil.getRelativePath(workingDir, file);
normalizedActualPaths.add(FileUtil.normalize(relativePath));
}
}
String[] actualPaths = normalizedActualPaths.toArray(new String[normalizedActualPaths.size()]);
Arrays.sort(actualPaths);
for (int i = 0; i < expectedPaths.length; i++) {
expectedPaths[i] = FileUtil.normalize(expectedPaths[i]);
}
Arrays.sort(expectedPaths);
String expected = StringUtil.join(expectedPaths, "\n");
String actual = StringUtil.join(actualPaths, "\n");
Assert.assertEquals("Compiled files differ", expected, actual);
}
});
}
}
@@ -0,0 +1,77 @@
package org.jetbrains.kotlin.maven;
import com.intellij.openapi.util.io.FileUtil;
import kotlin.io.TextStreamsKt;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.*;
import static org.jetbrains.kotlin.maven.MavenTestUtils.getNotNullSystemProperty;
class MavenProject {
@NotNull
private final File workingDir;
MavenProject(@NotNull String name) throws IOException {
File originalProjectDir = new File("src/test/resources/" + name);
workingDir = FileUtil.createTempDirectory("maven-test-" + name, null);
File[] filesToCopy = originalProjectDir.listFiles();
for (File from : filesToCopy) {
File to = new File(workingDir, from.getName());
FileUtil.copyFileOrDir(from, to);
}
}
@NotNull
File file(@NotNull String path) {
return new File(workingDir, path);
}
MavenExecutionResult exec(String... targets) throws Exception {
List<String> cmd = buildCmd(targets);
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
setUpEnvVars(processBuilder.environment());
processBuilder.directory(workingDir);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String stdout = TextStreamsKt.readText(reader);
int exitCode = process.waitFor();
return new MavenExecutionResult(stdout, workingDir, exitCode);
}
private void setUpEnvVars(Map<String, String> env) throws IOException {
String mavenHome = getNotNullSystemProperty("maven.home");
env.put("M2_HOME", mavenHome);
String mavenPath = mavenHome + File.separator + "bin";
env.put("PATH", env.get("PATH") + File.pathSeparator + mavenPath);
}
private List<String> buildCmd(String... args) {
List<String> cmd = new ArrayList<String>();
String osName = getNotNullSystemProperty("os.name");
if (osName.contains("Windows")) {
cmd.addAll(Arrays.asList("cmd", "/C"));
}
else {
cmd.add("/bin/bash");
}
cmd.add("mvn");
cmd.add("-Dkotlin.compiler.incremental.log.level=info");
String kotlinVersion = getNotNullSystemProperty("kotlin.version");
cmd.add("-Dkotlin.version=" + kotlinVersion);
cmd.addAll(Arrays.asList(args));
return cmd;
}
}
@@ -0,0 +1,33 @@
package org.jetbrains.kotlin.maven;
import kotlin.io.FilesKt;
import kotlin.text.Charsets;
import org.jetbrains.annotations.NotNull;
import java.io.*;
class MavenTestUtils {
@NotNull
static String readText(@NotNull File file) throws IOException {
return FilesKt.readText(file, Charsets.UTF_8);
}
static void writeText(@NotNull File file, @NotNull String text) throws IOException {
FilesKt.writeText(file, text, Charsets.UTF_8);
}
static void replaceFirstInFile(@NotNull File file, @NotNull String regex, @NotNull String replacement) throws IOException {
String text = readText(file);
String processedText = text.replaceFirst(regex, replacement);
writeText(file, processedText);
}
@NotNull
static String getNotNullSystemProperty(@NotNull String propertyName) {
String value = System.getProperty(propertyName);
if (value == null) {
throw new IllegalStateException("A system property '" + propertyName + "' is not set");
}
return value;
}
}
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>test-kotlin-incremental</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.2</version>
</plugin>
</plugins>
</pluginManagement>
<sourceDirectory>${project.basedir}/src</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,3 @@
class A {
fun foo(s: String) = s + s
}
@@ -0,0 +1,3 @@
fun useA() {
A().foo("Hello, world")
}