Clean up stale output files from Java compilation after Gradle version change - #1074
Clean up stale output files from Java compilation after Gradle version change#1074bmuschko wants to merge 48 commits into
Conversation
For now buildSrc will delete the output directories registered by the JavaPlugin and not buildDir.
Many empty directories were left behind which caused the build output cleanup to kick in and produce unexpected output.
Build output registry needs to be define for a build session so it can be shared between the main build and buildSrc.
|
Reviewed 34 of 34 files at r1. a discussion (no related file): subprojects/core/src/main/java/org/gradle/cache/internal/FileLock.java, line 54 at r1 (raw file):
This seems a little wonky. What changes would a file lock detect? subprojects/core/src/main/java/org/gradle/cache/internal/filelock/Version1LockStateSerializer.java, line 61 at r1 (raw file):
shouldn't this be based on whether or not we found a file lock state? subprojects/core/src/main/java/org/gradle/initialization/buildsrc/BuildSrcBuildListenerFactory.java, line 35 at r1 (raw file):
I'm not sure if we can remove the previous "clean everything" behavior for buildSrc because we're not fixing all possible inconsistencies yet. So someone could have a custom plugin they use in buildSrc that will now be broken without a clean. subprojects/core/src/main/java/org/gradle/internal/cleanup/BuildOutputCleanupCache.java, line 29 at r1 (raw file):
I'm not sure what this comment means. Does it mean that subprojects/core/src/main/java/org/gradle/internal/cleanup/BuildOutputCleanupListener.java, line 22 at r1 (raw file):
could this happen any later? e.g., after the DAG has been created? I think we want to avoid doing work that will be counted as "configuration" time and it might also be useful to show something in the progress logger when we do this. subprojects/core/src/main/java/org/gradle/internal/cleanup/DefaultBuildOutputCleanupCache.java, line 41 at r1 (raw file):
I think this isn't quite right. This is supposed to be the project cache dir? The project cache dir can be something other than subprojects/core/src/main/java/org/gradle/internal/cleanup/DefaultBuildOutputCleanupRegistry.java, line 27 at r1 (raw file):
would this make sense as a subprojects/core/src/test/groovy/org/gradle/initialization/buildsrc/BuildSourceBuilderTest.groovy, line 53 at r1 (raw file):
is it ok this is relative to the test JVM? subprojects/core/src/test/groovy/org/gradle/internal/cleanup/BuildOutputCleanupCacheTest.groovy, line 31 at r1 (raw file):
does this need a @UsesNativeServices? subprojects/core/src/test/groovy/org/gradle/internal/cleanup/DefaultBuildOutputDeleterTest.groovy, line 36 at r1 (raw file):
Maybe another/better way to do this (instead of relying on the logger sensing) is to make Then you can assert that the correct things were deleted by inspecting subprojects/docs/src/samples/userguideOutput/incrementalBuildInputFilesConfig.out, line 1 at r1 (raw file):
What change makes this up-to-date now? subprojects/integ-test/src/integTest/groovy/org/gradle/integtests/StaleOutputHistoryLossIntegrationTest.groovy, line 33 at r1 (raw file):
It'll be useful to see what does the failure look like when we can't properly clean-up the stale files. subprojects/integ-test/src/integTest/groovy/org/gradle/integtests/samples/UserGuideSamplesRunner.groovy, line 140 at r1 (raw file):
any idea why we don't use our own Delete stuff here? subprojects/plugins/src/main/java/org/gradle/api/plugins/JavaPlugin.java, line 208 at r1 (raw file):
Now that I've seen how this fits with other things.. I'm wondering if we should make all of these subprojects/plugins/src/main/java/org/gradle/api/plugins/JavaPlugin.java, line 225 at r1 (raw file):
Could we add a
That's probably the other common output type we have. Comments from Reviewable |
|
Sorry @bmuschko. I thought the comments would appear as normal. I've transferred them to the files on GH. |
| * An immutable snapshot of the state of a lock. | ||
| */ | ||
| interface State { | ||
| boolean canDetectChanges(); |
There was a problem hiding this comment.
This seems a little wonky. What changes would a file lock detect?
| } | ||
|
|
||
| public boolean canDetectChanges() { | ||
| return false; |
There was a problem hiding this comment.
shouldn't this be based on whether or not we found a file lock state?
| Listener create(boolean rebuild) { | ||
| return new Listener(rebuild); | ||
| Listener create() { | ||
| return new Listener(); |
There was a problem hiding this comment.
I'm not sure if we can remove the previous "clean everything" behavior for buildSrc because we're not fixing all possible inconsistencies yet. So someone could have a custom plugin they use in buildSrc that will now be broken without a clean.
There was a problem hiding this comment.
I am not sure I quite understand this point. We are deleting the buildDir directory. That's what the clean task does. Are you saying that there could be some logic that hooks into the clean task which now wouldn't happen anymore?
| * Cleans stale output files from previous builds. Stale files are left behind under the following conditions: | ||
| * | ||
| * - The task history is deleted and one or more input files are removed. | ||
| * - The build upgrades or downgrades to a different Gradle version, wasn't executed with that version before and one or more input files are removed. |
There was a problem hiding this comment.
I'm not sure what this comment means. Does it mean that clean() doesn't clean-up stale output files in these conditions or that these are the conditions that stale files are left behind?
There was a problem hiding this comment.
These are the conditions under which stale files/directories are left behind as indicated by the previous sentence. I'll move it to a new line to make it more clear.
| import org.gradle.api.internal.GradleInternal; | ||
| import org.gradle.initialization.ModelConfigurationListener; | ||
|
|
||
| public class BuildOutputCleanupListener implements ModelConfigurationListener { |
There was a problem hiding this comment.
could this happen any later? e.g., after the DAG has been created? I think we want to avoid doing work that will be counted as "configuration" time and it might also be useful to show something in the progress logger when we do this.
| @@ -1,4 +1,4 @@ | |||
| :clean | |||
| :clean UP-TO-DATE | |||
There was a problem hiding this comment.
What change makes this up-to-date now?
There was a problem hiding this comment.
This is related to delete.includeEmptyDirs = true. We didn't delete the directory before, only the contents of the directory. The sample is executed multiple times via UserGuideSamplesRunner.
| import static org.gradle.integtests.fixtures.StaleOutputJavaProject.* | ||
| import static org.gradle.util.GFileUtils.forceDelete | ||
|
|
||
| class StaleOutputHistoryLossIntegrationTest extends AbstractIntegrationSpec { |
There was a problem hiding this comment.
It'll be useful to see what does the failure look like when we can't properly clean-up the stale files.
There was a problem hiding this comment.
I tested this use case in DefaultBuildOutputDeleterTest.logs warning if file cannot be deleted - not necessarily on the functional test level but one level lower. Do you think that's sufficient?
| delete.dir = rootProjectDir | ||
| delete.includes = "**/.gradle/** **/build/**" | ||
| delete.includeEmptyDirs = true | ||
| delete.addFileset(new FileSet(dir: rootProjectDir, includes: "**/.gradle/** **/build/**")) |
There was a problem hiding this comment.
any idea why we don't use our own Delete stuff here?
There was a problem hiding this comment.
We are directly invoking the Ant task here: AntUtil.execute(delete). It's not a practice we promote in Gradle as it would mean calling the execute() method explicitly. Other than that we don't we have a method in GFileUtils or FileUtils that accepts a pattern.
| } | ||
| } | ||
|
|
||
| private class SourceSetOutputCleanUpRegistrationListener implements ProjectEvaluationListener { |
There was a problem hiding this comment.
Now that I've seen how this fits with other things.. I'm wondering if we should make all of these ModelConfigurationListeners so that we don't have to worry about a different afterEvaluate adding more configuration later (ignoring the fact we also have configure tasks...).
There was a problem hiding this comment.
If registration would support somewhat the same things as org.gradle.api.Project#files(java.lang.Object...) - i.e. some lazy evaluation - we could register the outputs right when we apply the plugin and evaluate them whenever we want to delete them. If we don't do that then we have the problem to decide when we want to evaluate the outputs. We would only evaluate all those paths when we really need to clean. WDYT?
|
|
||
| @Override | ||
| public void afterEvaluate(Project project, ProjectState state) { | ||
| buildOutputCleanupRegistry.registerOutputs(main.getOutput().getClassesDir(), main.getOutput().getResourcesDir(), test.getOutput().getClassesDir(), test.getOutput().getResourcesDir()); |
There was a problem hiding this comment.
Could we add a registerOutputs that accepts FileCollection, so this becomes:
buildOutputCleanupRegistry.registerOutputs(main.getOutput(), test.getOutput())
That's probably the other common output type we have.
There was a problem hiding this comment.
I removed the unused method that takes List<File> in favor of FileCollection. Good idea.
wolfs
left a comment
There was a problem hiding this comment.
As I understand it we would now cleanup the main and test classes directory after a version upgrade and cleanup the buildSrc/build directory, too. This would fix at least the most pressing problems.
With the current data captured for outputs we currently cannot address the empty sources problem since this could happen anytime even without a Gradle version change. That's OK for now.
I am not sure if we should explore deleting the directories any later than the end of configuration phase or if we also would leave that for a later PR. WDYT?
| } | ||
|
|
||
| private String createMessage(String type, File output) { | ||
| return String.format("Cleaned up %s '%s'", type, output); |
There was a problem hiding this comment.
Instead of using String.format can't we just use the message "Cleaned up {} '{}'" and then pass the two parameters to the logger.quiet method?
| logger.quiet(createMessage("file", output)); | ||
| } | ||
| } catch (UncheckedIOException e) { | ||
| logger.warn(String.format("Unable to clean up '%s'", output)); |
There was a problem hiding this comment.
Instead of using String.format can't we just use the message "Cleaned up {} '{}'" and then pass the two parameters to the logger.warn method?
| @NotYetImplemented | ||
| @Issue("GRADLE-1501") | ||
| def "production sources files are removed"() { | ||
| @Unroll |
There was a problem hiding this comment.
You can put @Unroll on the class level.
| } | ||
| """ | ||
|
|
||
| file('settings.gradle') << "include ${(1..projectCount).collect { "'${createProjectName(it)}'" }.join(',')}" |
There was a problem hiding this comment.
It is possible to create a multiproject build with a correct settings.gradle by using org.gradle.integtests.fixtures.AbstractIntegrationSpec#multiProjectBuild(java.lang.String, java.util.List<java.lang.String>, groovy.lang.Closure).
| arguments | description | ||
| [JAR_TASK_NAME] | 'without additional argument' | ||
| [JAR_TASK_NAME, '--parallel'] | 'in parallel' | ||
| [JAR_TASK_NAME, '--parallel', '--configure-on-demand'] | 'in parallel and configure on demand enabled' |
There was a problem hiding this comment.
For me it would be interesting to see what happens when we apply the java plugin in a separate build file (not the top level build file but a separate one in a subproject) and then only run the jar task in one of the subprojects. I would suppose that then only that classes directory gets cleaned while when running the jar task in one of the other projects after that the stale output files are still present.
| } | ||
| } | ||
|
|
||
| private class SourceSetOutputCleanUpRegistrationListener implements ProjectEvaluationListener { |
There was a problem hiding this comment.
If registration would support somewhat the same things as org.gradle.api.Project#files(java.lang.Object...) - i.e. some lazy evaluation - we could register the outputs right when we apply the plugin and evaluate them whenever we want to delete them. If we don't do that then we have the problem to decide when we want to evaluate the outputs. We would only evaluate all those paths when we really need to clean. WDYT?
| !result.output.contains(javaProject.classesDirCleanupMessage) | ||
| javaProject.mainClassFile.assertIsFile() | ||
| javaProject.redundantClassFile.assertIsFile() | ||
| hasDescendants(javaProject.jarFile, javaProject.mainClassFile.name, javaProject.redundantClassFile.name) |
There was a problem hiding this comment.
We are asserting the same things over and over again. Can't we extract those to assertions on StaleOutputJavaProject?
We could have something linke javaProjectFixture.assertBuiltWithRedundantFile, javaProjectFixture.assertBuiltWithoutRedundantFile and javaProjectFixture.assertCleanupExecuted/assertCleanupNotExecuted. WDYT?
| package org.gradle.internal.cleanup; | ||
|
|
||
| import java.io.File; | ||
| import java.util.List; |
|
OBE, a version of this was pushed to release (for 3.4) |
Implementation for #1018.