Skip to content

Commit e29a1cf

Browse files
committed
Changed mechanism for detecting obsolete class files.
Relying only on the source namespace name doesn't work for class files generated e.g. from defprotocol. Instead all files are compiled to a temporary directory and then copied to the destination directory. During next compilation first all files found in temporary destination directory are deleted from the proper destination directory.
1 parent 683b709 commit e29a1cf

File tree

9 files changed

+139
-43
lines changed

9 files changed

+139
-43
lines changed

src/main/kotlin/cursive/ClojurePlugin.kt

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ import org.gradle.api.tasks.SourceSet
3434
import org.gradle.api.tasks.TaskAction
3535
import org.gradle.api.tasks.compile.AbstractCompile
3636
import org.gradle.api.tasks.incremental.IncrementalTaskInputs
37-
import org.gradle.api.tasks.incremental.InputFileDetails
3837
import org.gradle.process.JavaForkOptions
3938
import org.gradle.process.internal.DefaultJavaForkOptions
4039
import org.gradle.process.internal.ExecException
4140
import org.gradle.process.internal.JavaExecHandleBuilder
4241
import org.gradle.util.ConfigureUtil
4342
import java.io.File
43+
import java.io.IOException
4444
import java.io.OutputStream
4545
import java.util.*
4646
import java.util.regex.Pattern
@@ -49,6 +49,7 @@ import javax.inject.Inject
4949
/**
5050
* @author Colin Fleming
5151
*/
52+
@Suppress("unused")
5253
class ClojurePlugin : Plugin<Project> {
5354
val logger = Logging.getLogger(this.javaClass)
5455

@@ -163,6 +164,12 @@ open class ClojureSourceSetImpl(displayName: String?, resolver: FileResolver?) :
163164

164165
class ReflectionWarnings(var enabled: Boolean, var projectOnly: Boolean, var asErrors: Boolean)
165166

167+
object FileCopyErrorHandler : (File, IOException) -> OnErrorAction {
168+
override fun invoke(file: File, exception: IOException): OnErrorAction {
169+
throw ExecException("Could not copy ${file} to output directory", exception)
170+
}
171+
}
172+
166173
open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
167174
AbstractCompile(),
168175
JavaForkOptions by DefaultJavaForkOptions(fileResolver) {
@@ -175,30 +182,38 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
175182
var elideMeta: Collection<String> = emptyList()
176183
var directLinking: Boolean = false
177184

185+
@Suppress("unused")
178186
var namespaces: Collection<String> = emptyList()
179187

188+
@Suppress("unused")
180189
fun reflectionWarnings(configureClosure: Closure<Any?>?): ReflectionWarnings {
181190
ConfigureUtil.configure(configureClosure, reflectionWarnings)
182191
return reflectionWarnings
183192
}
184193

185-
override fun compile() {
186-
throw UnsupportedOperationException()
187-
}
188-
194+
@Suppress("UNUSED_PARAMETER")
189195
@TaskAction
190196
fun compile(inputs: IncrementalTaskInputs) {
197+
compile()
198+
}
199+
200+
override fun compile() {
191201
logger.info("Starting ClojureCompile task")
192202

193-
destinationDir.mkdirs()
203+
val tmpDestinationDir = temporaryDir.resolve("classes")
204+
removeObsoleteClassFiles(destinationDir, tmpDestinationDir)
194205

195-
inputs.outOfDate { removeOutputFilesDerivedFromInputFile(it, destinationDir) }
196-
inputs.removed { removeOutputFilesDerivedFromInputFile(it, destinationDir) }
206+
if (!tmpDestinationDir.deleteRecursively()) {
207+
throw ExecException("Could not delete ${tmpDestinationDir}")
208+
}
209+
tmpDestinationDir.mkdirs()
210+
destinationDir.mkdirs()
197211

198212
if (copySourceToOutput ?: !aotCompile) {
199213
project.copy {
200-
it.from(getSource()).into(destinationDir)
214+
it.from(getSource()).into(tmpDestinationDir)
201215
}
216+
copyToDestination(tmpDestinationDir)
202217
return
203218
}
204219

@@ -215,7 +230,7 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
215230
logger.info("Compiling " + namespaces.joinToString(", "))
216231

217232
val script = listOf("(try",
218-
" (binding [*compile-path* \"${destinationDir.canonicalPath}\"",
233+
" (binding [*compile-path* \"${tmpDestinationDir.canonicalPath}\"",
219234
" *warn-on-reflection* ${reflectionWarnings.enabled}",
220235
" *compiler-options* {:disable-locals-clearing $disableLocalsClearing",
221236
" :elide-meta [${elideMeta.map { ":$it" }.joinToString(" ")}]",
@@ -263,6 +278,8 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
263278

264279
executeScript(script, stdout, stderr)
265280

281+
copyToDestination(tmpDestinationDir)
282+
266283
if (libraryReflectionWarningCount > 0) {
267284
System.err.println("$libraryReflectionWarningCount reflection warnings from dependencies")
268285
}
@@ -272,28 +289,22 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
272289
}
273290
}
274291

275-
private fun removeOutputFilesDerivedFromInputFile(inputFileDetails: InputFileDetails, destinationDir: File) {
276-
val sourceAbsoluteFile = inputFileDetails.file
277-
if (isClojureSource(sourceAbsoluteFile)) {
278-
logger.debug("Removing class files for {}", inputFileDetails.file)
279-
val sourceCanonicalFileName = sourceAbsoluteFile.canonicalPath
280-
val sourceFileRoot = getSourceRootsFiles()
281-
.find { sourceCanonicalFileName.startsWith(it.canonicalPath) }
282-
?: throw IllegalStateException("No source root found for source file ${sourceAbsoluteFile}")
283-
val sourceRelativeFile = sourceAbsoluteFile.relativeTo(sourceFileRoot)
284-
val sourceRelativeDirectory = sourceRelativeFile.parentFile
285-
val sourceFileName = sourceAbsoluteFile.nameWithoutExtension
286-
destinationDir.resolve(sourceRelativeDirectory)
287-
.listFiles { file -> file.name.startsWith(sourceFileName) }
288-
?.forEach {
289-
logger.debug("Deleting derived file {}", it)
290-
it.delete()
291-
}
292-
}
292+
private fun copyToDestination(tmpDestinationDir: File) {
293+
tmpDestinationDir.copyRecursively(target = destinationDir, overwrite = true, onError = FileCopyErrorHandler)
293294
}
294295

295-
private fun isClojureSource(file: File): Boolean {
296-
return CLJ_EXTENSION_REGEX.matches(file.extension) && getSourceRoots().any { file.canonicalPath.startsWith(it) }
296+
private fun removeObsoleteClassFiles(destinationDir: File, tmpDestinationDir: File) {
297+
tmpDestinationDir.walkBottomUp().forEach {
298+
val relativeFile = it.relativeTo(tmpDestinationDir)
299+
val fileInDestination = destinationDir.resolve(relativeFile)
300+
if (fileInDestination.exists()) {
301+
if (fileInDestination.delete()) {
302+
logger.debug("Deleted obsolete output file {}", fileInDestination)
303+
} else {
304+
logger.warn("Couldn't delete obsolete output file {}", fileInDestination)
305+
}
306+
}
307+
}
297308
}
298309

299310
private fun executeScript(script: String, stdout: OutputStream, stderr: OutputStream) {
@@ -385,7 +396,6 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
385396
'\\' to "_BSLASH_",
386397
'?' to "_QMARK_")
387398

388-
val CLJ_EXTENSION_REGEX = "cljc?".toRegex()
389399
val DEMUNGE_MAP = CHAR_MAP.map { it.value to it.key }.toMap()
390400
val DEMUNGE_PATTERN = Pattern.compile(DEMUNGE_MAP.keys
391401
.sortedByDescending { it.length }
@@ -394,6 +404,7 @@ open class ClojureCompile @Inject constructor(val fileResolver: FileResolver) :
394404

395405
val REFLECTION_WARNING_PREFIX = "Reflection warning, "
396406

407+
@Suppress("unused")
397408
fun munge(name: String): String {
398409
val sb = StringBuilder()
399410
for (c in name) {
@@ -431,7 +442,9 @@ open class ClojureTestRunner @Inject constructor(val fileResolver: FileResolver)
431442
ConventionTask(),
432443
JavaForkOptions by DefaultJavaForkOptions(fileResolver) {
433444

445+
@Suppress("unused")
434446
var classpath: FileCollection = SimpleFileCollection()
447+
@Suppress("unused")
435448
var namespaces: Collection<String> = emptyList()
436449
var junitReport: File? = null
437450

src/test/int-tests-projects/cursive/BasicClojureProjectTest/src/main/clojure/basic_project/core.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
(ns basic-project.core)
1818

19+
(defprotocol ITest)
20+
1921
(defn hello [name]
2022
(println "Generating message for" name)
2123
(str "Hello " name))

src/test/int-tests-projects/cursive/IncrementalCompilationTest/src/main/clojure/basic_project/core.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@
1616

1717
(ns basic-project.core)
1818

19+
(defprotocol ITest)
20+
1921
(defn hello [name]
2022
(str "Hello " name))

src/test/int-tests-projects/cursive/MixedJavaClojureTest/src/cljSS/clojure/cljSS/core.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
;
1616

1717
(ns cljSS.core
18-
(:import javaSS.Example))
18+
(:import (javaSS Example1 Example2)))
1919

2020
(defn test []
21-
(.test (Example.)))
21+
(.test (Example1.))
22+
(.test (Example2.)))

src/test/int-tests-projects/cursive/MixedJavaClojureTest/src/javaSS/java/javaSS/Example.java renamed to src/test/int-tests-projects/cursive/MixedJavaClojureTest/src/javaSS/java/javaSS/Example1.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package javaSS;
1818

19-
public class Example {
19+
public class Example1 {
2020
public void test() {
2121
}
2222
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2017 Colin Fleming
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package javaSS;
18+
19+
public class Example2 {
20+
public void test() {
21+
}
22+
}

src/test/kotlin/cursive/BasicClojureProjectTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package cursive
1818

1919
import org.assertj.core.api.KotlinAssertions.assertThat
2020
import org.gradle.testkit.runner.TaskOutcome
21+
import org.junit.Assert.assertTrue
2122
import org.junit.Test
2223

2324
class BasicClojureProjectTest : IntegrationTestBase() {
@@ -52,6 +53,7 @@ class BasicClojureProjectTest : IntegrationTestBase() {
5253
assertThat(result.task(":testClojure").outcome).isEqualTo(TaskOutcome.SUCCESS)
5354

5455
assertSourceFileIsOnlyCompiledToOutputDir(coreNsSourceFile)
56+
assertTrue("Protocol class file exists", testProjectDir.resolve("build/classes/main/basic_project/core/ITest.class").exists())
5557

5658
assertThat(result.output).contains("Generating message for World")
5759
}

src/test/kotlin/cursive/IncrementalCompilationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package cursive
1818

1919
import org.assertj.core.api.KotlinAssertions.assertThat
2020
import org.gradle.testkit.runner.TaskOutcome
21+
import org.junit.Assert.assertFalse
2122
import org.junit.Test
2223

2324
class IncrementalCompilationTest : IntegrationTestBase() {
@@ -113,5 +114,6 @@ class IncrementalCompilationTest : IntegrationTestBase() {
113114
assertSourceFileNotCopiedToOutputDir(coreNsSourceFile)
114115
assertSourceFileNotCompiledToOutputDir(coreNsSourceFile)
115116
assertSourceFileIsOnlyCompiledToOutputDir(utilsNsSourceFile)
117+
assertFalse("Protocol class file doesn't exists", testProjectDir.resolve("build/classes/main/basic_project/core/ITest.class").exists())
116118
}
117119
}

src/test/kotlin/cursive/MixedJavaClojureTest.kt

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,76 @@ import org.gradle.testkit.runner.TaskOutcome
2121
import org.junit.Test
2222

2323
class MixedJavaClojureTest : IntegrationTestBase() {
24-
@Test
25-
fun `Compilation with Clojure code depending on Java code`() {
26-
// given
27-
val javaOutputDir = testProjectDir.resolve("build/classes/javaSS")
28-
val javaClassFile = javaOutputDir.resolve("javaSS/Example.class")
24+
val javaOutputDir = testProjectDir.resolve("build/classes/javaSS")
25+
val javaExample1SourceFile = testProjectDir.resolve("src/javaSS/java/javaSS/Example1.java")
26+
val javaExample1ClassFile = javaOutputDir.resolve("javaSS/Example1.class")
27+
val javaExample2ClassFile = javaOutputDir.resolve("javaSS/Example2.class")
2928

30-
val cljSourceDir = testProjectDir.resolve("src/cljSS/clojure")
31-
val cljCoreNsFile = cljSourceDir.resolve("cljSS/core.clj")
32-
val cljOutputDir = testProjectDir.resolve("build/classes/cljSS")
29+
val cljSourceDir = testProjectDir.resolve("src/cljSS/clojure")
30+
val cljCoreNsFile = cljSourceDir.resolve("cljSS/core.clj")
31+
val cljOutputDir = testProjectDir.resolve("build/classes/cljSS")
3332

33+
@Test
34+
fun `Compilation with Clojure code depending on Java code`() {
3435
// when
3536
val result = projectBuildRunner().withArguments("compileCljSSClojure").build()
3637

3738
// then
3839
assertThat(result.task(":compileJavaSSJava").outcome).isEqualTo(TaskOutcome.SUCCESS)
3940
assertThat(result.task(":compileCljSSClojure").outcome).isEqualTo(TaskOutcome.SUCCESS)
4041

41-
assertThat(javaClassFile.exists()).isTrue()
42+
assertThat(javaExample1ClassFile.exists()).isTrue()
43+
assertThat(javaExample2ClassFile.exists()).isTrue()
44+
assertSourceFileIsOnlyCompiledToOutputDir(cljCoreNsFile, cljSourceDir, cljOutputDir)
45+
}
46+
47+
@Test
48+
fun `Incremental compilation with Clojure code depending on Java code when Java source unchanged`() {
49+
// when
50+
val firstResult = projectBuildRunner().withArguments("compileCljSSClojure").build()
51+
52+
// then
53+
assertThat(firstResult.task(":compileJavaSSJava").outcome).isEqualTo(TaskOutcome.SUCCESS)
54+
assertThat(firstResult.task(":compileCljSSClojure").outcome).isEqualTo(TaskOutcome.SUCCESS)
55+
56+
assertThat(javaExample1ClassFile.exists()).isTrue()
57+
assertThat(javaExample2ClassFile.exists()).isTrue()
4258
assertSourceFileIsOnlyCompiledToOutputDir(cljCoreNsFile, cljSourceDir, cljOutputDir)
59+
60+
// when
61+
val secondResult = projectBuildRunner().withArguments("compileCljSSClojure").build()
62+
63+
// then
64+
println(secondResult.output)
65+
assertThat(secondResult.task(":compileJavaSSJava").outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
66+
assertThat(secondResult.task(":compileCljSSClojure").outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
67+
68+
assertThat(javaExample1ClassFile.exists()).isTrue()
69+
assertThat(javaExample2ClassFile.exists()).isTrue()
70+
assertSourceFileIsOnlyCompiledToOutputDir(cljCoreNsFile, cljSourceDir, cljOutputDir)
71+
}
72+
73+
@Test
74+
fun `Incremental compilation with Clojure code depending on Java code when Java source changes`() {
75+
// when
76+
val firstResult = projectBuildRunner().withArguments("compileCljSSClojure").build()
77+
78+
// then
79+
assertThat(firstResult.task(":compileJavaSSJava").outcome).isEqualTo(TaskOutcome.SUCCESS)
80+
assertThat(firstResult.task(":compileCljSSClojure").outcome).isEqualTo(TaskOutcome.SUCCESS)
81+
82+
assertThat(javaExample1ClassFile.exists()).isTrue()
83+
assertThat(javaExample2ClassFile.exists()).isTrue()
84+
assertSourceFileIsOnlyCompiledToOutputDir(cljCoreNsFile, cljSourceDir, cljOutputDir)
85+
86+
// when
87+
javaExample1SourceFile.delete()
88+
val secondResult = projectBuildRunner().withArguments("compileCljSSClojure").buildAndFail()
89+
90+
// then
91+
assertThat(secondResult.task(":compileJavaSSJava").outcome).isEqualTo(TaskOutcome.SUCCESS)
92+
assertThat(secondResult.task(":compileCljSSClojure").outcome).isEqualTo(TaskOutcome.FAILED)
93+
94+
assertThat(secondResult.output).contains("java.lang.ClassNotFoundException: javaSS.Example1")
4395
}
4496
}

0 commit comments

Comments
 (0)