Skip to content

Clean up stale output files from Java compilation after Gradle version change - #1074

Closed
bmuschko wants to merge 48 commits into
masterfrom
bm-stale-output-cleanup
Closed

Clean up stale output files from Java compilation after Gradle version change#1074
bmuschko wants to merge 48 commits into
masterfrom
bm-stale-output-cleanup

Conversation

@bmuschko

Copy link
Copy Markdown
Contributor

Implementation for #1018.

Benjamin Muschko added 30 commits December 13, 2016 18:54
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.
@bmuschko

Copy link
Copy Markdown
Contributor Author

@wolfs @big-guy Looking for some early feedback. Thanks!

@bmuschko
bmuschko requested review from big-guy and wolfs January 3, 2017 12:35
@bmuschko bmuschko self-assigned this Jan 3, 2017
@bmuschko bmuschko added this to the 4.0 RC1 milestone Jan 3, 2017
@big-guy

big-guy commented Jan 9, 2017

Copy link
Copy Markdown
Member

Reviewed 34 of 34 files at r1.
Review status: all files reviewed at latest revision, 16 unresolved discussions, some commit checks failed.


a discussion (no related file):
I think we should add some coverage for composite builds into this since we have a new listener that we need to make sure is triggered at the right times.


subprojects/core/src/main/java/org/gradle/cache/internal/FileLock.java, line 54 at r1 (raw file):

     */
    interface State {
        boolean canDetectChanges();

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):

        public boolean canDetectChanges() {
            return false;

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):

    private static final String DEFAULT_BUILD_SOURCE_SCRIPT_RESOURCE = "defaultBuildSourceScript.txt";

    Listener create() {

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):

     *
     * - 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.

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?


subprojects/core/src/main/java/org/gradle/internal/cleanup/BuildOutputCleanupListener.java, line 22 at r1 (raw file):

import org.gradle.initialization.ModelConfigurationListener;

public class BuildOutputCleanupListener implements ModelConfigurationListener {

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):

    public DefaultBuildOutputCleanupCache(CacheRepository cacheRepository, File cacheBaseDir, BuildOutputDeleter buildOutputDeleter, BuildOutputCleanupRegistry buildOutputCleanupRegistry) {
        this.cacheRepository = cacheRepository;
        this.cacheDir = new File(cacheBaseDir, ".gradle/noVersion/buildOutputCleanup");

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 .gradle/, I think we're supposed to use a different CacheRepository method to make this cache Gradle or Project scope.


subprojects/core/src/main/java/org/gradle/internal/cleanup/DefaultBuildOutputCleanupRegistry.java, line 27 at r1 (raw file):

public class DefaultBuildOutputCleanupRegistry implements BuildOutputCleanupRegistry {

    private final List<File> outputs = new ArrayList<File>();

would this make sense as a Set?


subprojects/core/src/test/groovy/org/gradle/initialization/buildsrc/BuildSourceBuilderTest.groovy, line 53 at r1 (raw file):

    void "creates classpath when build src exists"() {
        def classpath = [new File('test')]

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):

import spock.lang.Specification

class BuildOutputCleanupCacheTest extends Specification {

does this need a @UsesNativeServices?


subprojects/core/src/test/groovy/org/gradle/internal/cleanup/DefaultBuildOutputDeleterTest.groovy, line 36 at r1 (raw file):

    def setup() {
        buildOutputDeleter.logger = logger

Maybe another/better way to do this (instead of relying on the logger sensing) is to make deleteOutput protected and override it here like:

def deletedFiles = []
def buildOutputDeleter = new DefaultBuildOutputDeleter() {
   void deleteOutput(File file) {
      deletedFiles << file
   }
}

Then you can assert that the correct things were deleted by inspecting deletedFiles.


subprojects/docs/src/samples/userguideOutput/incrementalBuildInputFilesConfig.out, line 1 at r1 (raw file):

:clean UP-TO-DATE

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):

import static org.gradle.util.GFileUtils.forceDelete

class StaleOutputHistoryLossIntegrationTest extends AbstractIntegrationSpec {

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):

                delete.includeEmptyDirs = true
                delete.addFileset(new FileSet(dir: rootProjectDir, includes: "**/.gradle/** **/build/**"))
                AntUtil.execute(delete)

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):

    }

    private class SourceSetOutputCleanUpRegistrationListener implements ProjectEvaluationListener {

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...).


subprojects/plugins/src/main/java/org/gradle/api/plugins/JavaPlugin.java, line 225 at r1 (raw file):

        @Override
        public void afterEvaluate(Project project, ProjectState state) {
            buildOutputCleanupRegistry.registerOutputs(main.getOutput().getClassesDir(), main.getOutput().getResourcesDir(), test.getOutput().getClassesDir(), test.getOutput().getResourcesDir());

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.


Comments from Reviewable

@big-guy

big-guy commented Jan 9, 2017

Copy link
Copy Markdown
Member

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little wonky. What changes would a file lock detect?

}

public boolean canDetectChanges() {
return false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What change makes this up-to-date now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be useful to see what does the failure look like when we can't properly clean-up the stale files.

@bmuschko bmuschko Jan 10, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/**"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why we don't use our own Delete stuff here?

@bmuschko bmuschko Jan 10, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the unused method that takes List<File> in favor of FileCollection. Good idea.

@wolfs wolfs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can put @Unroll on the class level.

}
"""

file('settings.gradle') << "include ${(1..projectCount).collect { "'${createProjectName(it)}'" }.join(',')}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import.

@bmuschko bmuschko assigned wolfs and big-guy and unassigned bmuschko and wolfs Jan 13, 2017
@wolfs wolfs modified the milestones: 3.4 RC1, 4.0 RC1 Jan 18, 2017
@big-guy big-guy closed this Jan 19, 2017
@big-guy

big-guy commented Jan 19, 2017

Copy link
Copy Markdown
Member

OBE, a version of this was pushed to release (for 3.4)

@bmuschko
bmuschko deleted the bm-stale-output-cleanup branch January 20, 2017 22:16
@ov7a ov7a removed this from the 3.4 RC1 milestone Mar 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants