Custom Task using copy closure results in ClassNotFoundException if executed immediately after clean

I am writing a custom task that involves calls to copy and exec closures. When the task is executed by itself it works as expected. However if it is combined with clean it results in a ClassNotFoundException.

Here is the code segment causing the exception:

getProject().copy {
            from buildFolder
            into deployWorkingDirectory
            include rawDeployName
            rename (rawDeployName, deployName)
        }

so “gradle clean” then “gradle customTask” works but “gradle clean customTask” gives this stacktrace:

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':myProject:deployExe'.
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
	at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
	at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:64)
	at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
	at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:42)
	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
	at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
	at org.gradle.api.internal.AbstractTask.executeWithoutThrowingTaskFailure(AbstractTask.java:310)
	at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.executeTask(AbstractTaskPlanExecutor.java:79)
	at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:63)
	at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:51)
	at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:23)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:88)
	at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
	at org.gradle.execution.DefaultBuildExecuter.access$200(DefaultBuildExecuter.java:23)
	at org.gradle.execution.DefaultBuildExecuter$2.proceed(DefaultBuildExecuter.java:68)
	at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:62)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:55)
	at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:149)
	at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:106)
	at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:86)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:90)
	at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)
	at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:41)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:28)
	at org.gradle.launcher.exec.DaemonUsageSuggestingBuildActionExecuter.execute(DaemonUsageSuggestingBuildActionExecuter.java:50)
	at org.gradle.launcher.exec.DaemonUsageSuggestingBuildActionExecuter.execute(DaemonUsageSuggestingBuildActionExecuter.java:27)
	at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:40)
	at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:169)
	at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:237)
	at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:210)
	at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:35)
	at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:24)
	at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:206)
	at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:169)
	at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
	at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
	at org.gradle.launcher.Main.doAction(Main.java:33)
	at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
	at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:54)
	at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:35)
	at org.gradle.launcher.GradleMain.main(GradleMain.java:23)
Caused by: java.lang.NoClassDefFoundError: com/deploytasks/MyDeployTask$_performUpload_closure4
	at com.deploytasks.MyDeployTask.performUpload(TbDeployTask.groovy:73)
	at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:75)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.doExecute(AnnotationProcessingTaskFactory.java:226)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:219)
	at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:208)
	at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:589)
	at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:572)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
	... 46 more
Caused by: java.lang.ClassNotFoundException: com.deploytasks.MyDeployTask$_performUpload_closure4
	... 55 more

This particular stacktrace was generated from Gradle 2.4, but I typically use 2.2.1 which was demonstrating a similar stacktrace (same exception). I imagine the issue is user error but I’m confused why combining the two commands into one is what presents the exception.

My suspicion would be that the “ClassNotFoundException” is really from some other underlying exception (file not found, null pointer, etc). It also looks like it could be when you run gradle clean customTask vs gradle clean && gradle customTask a file exists before any tasks run and your custom task does something different based on that during the configuration phase. After the clean executes, your custom task still expects the file to exist. When you run the clean separately, the file doesn’t exist and you don’t have a problem.

Can you post more of the performUpload method or a reproducible project?

Thanks for the recommendation sterling. The task did have a couple input File references during configuration (buildFolder and deployWorkingDirectory) however nothing is done to them until execution and checking them before the copy closure found them to be not null and to exist as directories. I tried just turning them into Strings that represent the paths and initializing the File objects during execution for the sake of isolation but that didn’t change the exception.

However I did rebuild a project to attempt to make the issue reproducible and (at least for now) it runs as expected. So it does seem this is user error.

However I did notice one inconsistency between the rebuilt project and the original one. In the rebuilt project I had to preface the copy closure with getProject() or else I would get:

Caused by: org.gradle.api.internal.MissingMethodException: Could not find method copy() for arguments [com.myproject.MyTask$_performUpload_closure2@30537a5a] on task ':copy_issue_sub:CustomTask'.

In the original project however I don’t seem to be required to have the prefacing getProject(). Without it there I still get the same exception:

Caused by: java.lang.ClassNotFoundException: com.originalproject.CustomTask$_performUpload_closure4

In both cases I was executing the same “gradle clean CustomTask.” Seeing as this seems related to the project object does this structural being amiss in the original project?

Edit:

Apparently this is another disjoint behavior between running it with the clean task and without. Running clean separately from CustomTask results in MissingMethodException which is what I would expect to occur in the original project. However running “gradle clean customTask” causes project to fail with ClassNotFoundException again.

Very strange. I still think the class not found is being caused by something else. Are you running with --stacktrace or --full-stacktrace? We might be clipping off the real cause when we shorten it for --stacktrace.

Since calling getProject() seems to be in the mix… are there any local variables/fields called ‘project’ near the copy? Are there many nested closures?

Would you mind sending me your custom task source? sterling.greene ‘at’ gradleware.com

Unfortunately I can’t disclose the source code and my attempts to recreate the issue in a demo project have yet to duplicate it so I don’t think sending the demo project would be worth much to you.

The --full-stacktrace yielded a little more output, but doesn’t seem to be particularly informative. Here is the final section:

Caused by: java.lang.ClassNotFoundException: com.original.project.DeployTask$_performUpload_closure4
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
... 70 more

I can describe more of the build process procedure however.

The custom task code is in buildSrc/src/main/groovy/ as documented in the user guide.

The root project is pretty bare with respect to itself, it adds some properties to ext in its allprojects block and sets the group. It does apply the base plugin so it would have the clean task which is has an appending closure to delete the directory all subprojects locally output their production applications. Lastly it defines a couple tasks that create new subprojects structured as we like them.

I tried removing application of the base plugin and the appending clean task closure in case having that in the root project was causing something odd to the project object, but it still had the same result.

Inside of subprojects we apply the java, maven, and eclipse plugs and some custom resolutionStrategy logic. We set custom sourceSet paths and define the tasks that integrate with our maven repo.

At this point I feel like this stuff is all pretty normal. But we have a section at the bottom of subprojects that does something similar to this:

afterEvaluate { project ->

    jar {
        manifest {
            attributes("Implementation-Version": project.version)
        }
    }
    
    if (project.hasProperty(flag1)) {
        project.apply from: rootProject.rootDir.path + File.separator + 'flag1.gradle'
    }

    if (project.hasProperty(flag2) && project.getTasks().findByPath('taskNameAddedByFlag2.gradle') == null) {
        project.apply from: rootProject.rootDir.path + File.separator + 'flag2.gradle'
    }

    if (project.hasProperty(flag3) && project.getTasks().findByPath('taskNameAddedByFlag3') == null) {
        project.apply from: rootProject.rootDir.path + File.separator + 'flag3.gradle'
    }
}

The findByPath conditionals were added because some subprojects of the subprojects of root were failing due to collision of task names because their parent (the direct subproject of root) already had them.
It’s inside of these applied scripts that we create instances of the custom task. So for example flag1.gradle would have this inside of it:

import com.myproject.MyTask

task CustomTask (dependsOn: jar, type: MyTask) {
    group = 'Test Tasks'
    def buildPath = [projectDir.path,'build','libs'].join(File.separator)
    buildFolderPath buildPath
    rawDeployName project.name +'.jar'
    def deployPath = [projectDir.path,'build','deploy-prep'].join(File.separator)
    deployWorkingDirectoryPath deployPath
    deployName 'deployName.jar'
}

And then flag2.gradle would have a variation of it for deployment of another kind of artifact:

import com.myproject.MyTask

task AnotherCustomTask (dependsOn: jar, type: MyTask) {
    group = 'Test Tasks'
    def buildPath = [projectDir.path,'build','libs'].join(File.separator)
    buildFolderPath buildPath
    rawDeployName project.name +'.jar'
    def deployPath = [projectDir.path,'build','deploy-prep'].join(File.separator)
    deployWorkingDirectoryPath deployPath
    deployName 'deployName.jar'
}

For completeness here is the MyTask definition under buildSrc in the demo project.

package com.myproject

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction


public class MyTask extends DefaultTask {
    
    String buildFolderPath
    String rawDeployName
    String deployName
    String deployWorkingDirectoryPath
        
    @TaskAction
    def performUpload () {
        
        if (rawDeployName == null || rawDeployName.isEmpty()) {
            rawDeployName = deployName
        }
        
        def buildFolder = new File (buildFolderPath)
        def deployWorkingDirectory = new File (deployWorkingDirectoryPath)

        println "Build: "+buildFolder
        println "Raw: "+rawDeployName
        println "Deploy name: "+deployName
        println "Deploy working dir: "+deployWorkingDirectory
        
        getProject().copy {
            from buildFolder
            into deployWorkingDirectory
            include rawDeployName
            rename (rawDeployName, deployName)
        }
        
        getProject().exec {
            //In reality this would call rsync and upload the above copied ('staged') artifact that was renamed with its production name. But it doesn't get that far
            workingDir deployWorkingDirectory
            executable = 'echo'
            args 'hi'
        }        
    }
}

However all of this works in the demo project. In the actual MyCustomTask class there are more closures assigned to variables, but inside the performUpload method the only nesting is into getProject().copy. In fact the other closures operate as expected and can even retrieve values out of the project object successfully.

I am hitting exact same error. Did you find out the solution ?

1 Like

Same here… I’m just using for loops now… it’s awful.