How to specify an output file for a class that implements Plugin?

I’m trying to author my first small Gradle plugin. I got the logic working as a task in build.gradle, and now I’d like to move it to a Groovy class. It looks like I want to implement Plugin<Project>, according to the documentation. I also want to specify an @OutputFile, but I don’t know how to do that correctly. I’ve tried:

@OutputFile
abstract RegularFile outputFile = project.layout.projectDirectory.file("output.txt")

since project.layout.projectDirectory.file() worked in build.gradle. However, it looks like project is not defined at the class level (makes sense since it’s an argument to apply). I couldn’t find anything in the documentation that addresses this. How should this be done?

If you haven’t already, I suggest reading the other topics under Developing Gradle Plugins as well. I think Implementing Plugins might be most applicable to your scenario.

In general, you want to create re-usable task classes that the plugin uses to register instances of based on some convention/pattern/model. For a basic example, I want a plugin that supplies a task for joining two text files. I’m going to assume the latest version of Gradle is used, so if you need to support older versions this code may not work.

First we need a task class to do the actual join.

abstract class FileJoinTask extends DefaultTask {
  @InputFile
  abstract RegularFileProperty getFirstFile()
  @InputFile
  abstract RegularFileProperty getSecondFile()
  @OutputFile
  abstract RegularFileProperty getOutputFile()
  @TaskAction
  void joinFiles() {
    File outFile = outputFile.get().asFile
    outFile.parentFile.mkdirs()
    outFile.text = firstFile.get().asFile.text + secondFile.get().asFile.text
  }
}

Now the plugin needs to register an instance of the FileJoinTask class. By convention I’ve decided that the plugin will join 2 files named one.txt and two.txt in the project directory and output the results to joined.txt in the project’s build directory. Modelling a plugin is usually more complex and involves adding project extensions, but I’ll leave that out just to focus on what you’re doing.

class FileJoinPlugin implements Plugin<Project> {
  @Override
  void apply(Project project) {
    project.tasks.register("joinFiles", FileJoinTask) { jt ->
      jt.group = "file joiner"
      jt.firstFile.convention(project.layout.projectDirectory.file("one.txt"))
      jt.secondFile.convention(project.layout.projectDirectory.file("two.txt"))
      jt.outputFile.convention(project.layout.buildDirectory.file("joined.txt"))
    }
  }
}

Ahhh, I didn’t realize a task needs to be in its own class; I thought the plugin class could contain all the logic. I’ll work to separate these concerns and see how that goes.

I have read through (at least at a high level) all the pages in Implementing Gradle Plugins, but it’s often confusing or seems to leave things out to a naive reader.