Run task if other is not up to date

I have started getting TaskInternal.execute() deprecation message and I’d like to move away from it, but I’m having trouble with rewriting my current task setup.

The way I use it now is:

task cleanFiles(type: Delete) {
    delete generatedDir
}

task generateFiles(type: Exec) {
    mustRunAfter cleanFiles

    inputs.files templateFiles
    outputs.dir generatedDir

    // run file generation

    doFirst {
        tasks.cleanFiles.execute()
    }
}

task regenerateFiles {
    dependsOn cleanFiles, generateFiles
}

I always need to clean files before generation, but I don’t want to clean them in case they are up to date. I also want to have a way to force file generation.

Could someone please point me to the right direction as to how to rewrite this without execute()?

1 Like

Think about what each task needs to do as a separate, incremental unit of work. You never need to cleanFiles if your only intent is to generateFiles. Rather, if you generateFiles, you need to delete anything that already exists in the generatedDir.

Rather than referring to another task (outside of the public task API), update your doFirst { } task action to do the actual work that you intend to do. i.e:

doFirst {
    project.delete(generatedDir)
}
1 Like

Thanks, that looks like a reasonable approach.

But what if file deletion wasn’t that trivial? Would I be left with having to keep 2 different blocks of code to do exactly the same thing?

I can’t even see how logic extraction would be possible, since it’s just configuration code in one case and actual execution in the other.

Is there a way to prevent duplicating the code?

There’s a lot that goes into file deletion. You can pass many files/folders to delete, follow symlinks if desired, and there’s logic to try to recover from failed deletes with nuances depending on the system platform. The operation seems trivial because the API is simple and hides most of the complexity from the client code / build script.

Behind the scenes, both the Delete task and the project.delete() method delegate much of their work to the same classes, such as the Deleter that does the traversal and deletion. One just provides an API appropriate for a task while the other provides immediate action, more suited for a task action.

Good object oriented design such as single responsibility still applies here. The one job of your tasks / actions should be to convert what makes sense in their context to what is understood by a collaborator that can actually do the work. You may have multiple clients to your code that does the heavy lifting, but this shouldn’t be a bad thing.

When a task executes, it executes one or more actions. The Delete task has an Action that takes the task configuration and does the actual execution with it. The project.delete() method gives you a public API to access the same behavior (just without abusing the Task implementation details).

There shouldn’t really be duplicated code. Multiple clients with different context calling the same API each have a valid use case that should justify their existence.

You should be able to follow these same principles if you’re implementing custom functionality from scratch.

There are many different levels at which you can expand the Gradle functionality available to your build. Some examples:

  • functions and classes declared in the build file
  • adding utility classes to the buildSrc project
  • adding external utility jars to the buildscript’s classpath dependencies
  • writing plugins directly in the build file, in buildSrc, or bringing them from external repositories

At the basic end of the spectrum, using your example of a “custom/complicated delete”, one solution could be to declare a function and then call it from both tasks.

void myDelete( File dir ) {
    // do special stuff, could include project.delete(...) if needed
}

task cleanFiles {  // NOTE: not a Delete task anymore
    File dir = generatedDir

    doLast {
        myDelete( dir )
    }
}

task generateFiles( type: Exec ) {
    File destDir = generatedDir
    ...
    doFirst {
        myDelete( destDir )
    }
}