Gradle says "Output file has been removed" -- but it hasn't been

I have a custom task that goes through a document source tree and generates revhistory.html for each revhistory.csv file it finds. I add the path to this generated file to an @OutputFiles field. However, it doesn’t avoid a build because whenever I run the task consecutively, it tells me:

Executing task ':revhistory' (up-to-date check took 0.062 secs) due to:
  Output file C:\Users\O386600\rtc\doc\src\dev\revhistory.html has been removed.
  Output file C:\Users\O386600\rtc\doc\src\ucd\revhistory.html has been removed.
  Output file C:\Users\O386600\rtc\doc\src\deplteam\revhistory.html has been removed.
Creating history at C:\Users\O386600\rtc\doc\src\deplteam\revhistory.html
Creating history at C:\Users\O386600\rtc\doc\src\dev\revhistory.html
Creating history at C:\Users\O386600\rtc\doc\src\ucd\revhistory.html

But when I look at these output file locations before running the task, they have not been removed, they’re sitting right there where I want them.

Any help on why it would be doing this?

Thank you!

Andy

For your custom task, are you not adding output files to your @OutputFiles field if you don’t need to generate them? I think we might use “has been removed” in two cases (where the output file has been deleted and where the output file is no longer in the output file list).

Hmm, yes, I guess that seems to be the problem. The task does not update @OutputFiles until it is already running. But isn’t it a quandary that the task can’t programmatically calculate @OutputFiles since the check happens during configuration? I specify the input files when creating a task of the custom type, but what is the right place to specify the output files in order to ensure that the need to build is determined?

Andy

p.s. – Thanks for your article on incremental builds! It was very informative but I’m still not getting the idea of how to best use @OutputFiles.

You can annotate methods too (not just fields) with @OutputFiles.

So if you have this in your custom task:

@OutputFiles FileCollection outputFiles

You can turn that into:

@OutputFiles 
FileCollection getOutputFiles() {
   // logic to calculate output files from source files
}

Glad you found the article helpful.

Great, I think I’m getting it now. Just one last question to clarify my understanding: is it correct that gradle checks this list of outputFiles during configuration phase in order to determine what needs to be done? It’s the job of the custom task to make a list of “notionally” existing output files and if they exist then the task does not run. If they don’t exist, it runs. Is that it?

Mostly. We collect the list of inputs/outputs just before the task executes. We then look for changed inputs (new inputs, missing inputs, modified inputs) and outputs (new outputs, missing outputs, modified outputs). If anything has changed, we re-run the task.

If you wanted to make your task incremental (only rebuild a subset of all inputs/outputs), there’s a way of creating a task that does that (https://docs.gradle.org/current/dsl/org.gradle.api.tasks.incremental.IncrementalTaskInputs.html). This can be tricky if there is some sort of dependency relationship between output files, but it’s pretty straightforward if it’s just a simple translation of input file to output file.

In either case, you want the task’s @OutputFiles to always be based on the inputs, not on what may already exist.

1 Like

Great, thanks, Sterling!

Ugh. . . I thought I was all set but apparently I’m still doing something wrong. It seems my function for @Outputfiles is not getting executed. My task looks like this:

public class RevHistory extends DefaultTask {
  @InputFiles
  FileTree revfiles

  def revision
  def revdate
  def revauthor
  def revremark

  @TaskAction
  public void generateRevHistory(){
    def revtable
    // for each revhistory file, we try to generate a revhistory.html
    revfiles.each{ csv->
      if(csv.name.endsWith("csv")){
        def newname = generateOutfile(csv)
        def outfile = new File(newname)
        if(!outfile.parentFile.exists()){
          outfile.parentFile.mkdirs()
        }
        (revtable,revision,revdate,revauthor,revremark) = makeRevHistory(csv,outfile)
        System.out.printf("Generated %s (at version %s) to list of outfiles%n",outfile,revision);
      }
    }
  }

  def generateOutfile(infile){
    def newname = String.format("%s/revhistory/%s/%s",project.getBuildDir(),infile.parentFile.name,infile.name.replaceAll(".csv",".html")).replaceAll("/",Matcher.quoteReplacement(File.separator))
  }

  /**
   * Determine which output files _should_ exist
   */
  @OutputFiles
  public FileCollection generateOutputFiles(){
    System.out.printf("Entered generateOutputfiles%n");
    FileCollection outfiles = []
    // output files should all be in the build directory in the same 
    revfiles.each{ csv->
      if(csv.name.endsWith("csv")){
        def newname = generateOutfile(csv)
        System.out.printf("Adding %s to list of outfiles%n",newname);
        outfiles << new File(newname)
      }
    }
    return outfiles
  }

  def makeRevHistory(File csv,File outfile){
    String.printf "Creating history at %s\n",outfile
    def lines = csv.readLines()
    def header = lines.first()
    def builder = new groovy.xml.MarkupBuilder(new PrintWriter(outfile))
    lines = lines.drop(1)
    builder.table{
      tr{
        header.split(";").each{
          th it
        }
      }
      lines.each{ row->
        tr{
          row.split(";").each{ cell->
            td cell
          }
        }
      }
    }
    // set the current attributes
    def current = lines[lines.size()-1]
    def curvals = current.split(";")
    return [builder,curvals[0],curvals[1],curvals[2],curvals[3]]
  }
}

But I never see the output I’d expect if generateOutputFiles() were called. Sorry to continue to pester, but am I at least going down the right track or maybe some misconception?

It needs to follow Java bean conventions and be called getSomething.

1 Like

Ah, got it… thanks again for sticking with me. It works now! :blush:

I am getting something similar. Seems like the logging stops at the 3rd file it cannot find. Gradle is saying the files are removed but they are all in the file system at the specified location.