Gradle file rename causes circular dependency error - Why?

I want to store an input file in a JAR created with a subclass of the Jar task in Gradle. The input file shall be stored under a different name.

Here is my build.gradle (complete working example; just create ‘dir1/file1.txt’ first):

task myjar(type: MyJarTask);
      class MyJarTask extends Jar {
  @InputFile
  File file1 = new File('dir1/file1.txt');
        public MyJarTask() {
    configure {
      baseName 'foo'
      from {
        file1;
        // comment out the next line to eliminate the error:
        rename { String fileName -> fileName.replace('file1.txt', 'file2.txt'); }
      }
    }
  }
}

Running this via ‘gradle myjar’ gives the following error:

* What went wrong:  Circular dependency between the following tasks:

:myjar

\--- :myjar (*)







(*) - details omitted (listed previously)

When I comment out the line with the ‘rename’, everything works! (Of course the file is not renamed.)

What is the reason for this surprising behavior? Are we witnessing a Gradle bug?
Please do not suggest alternative solutions; I solved the original problem by avoiding the need for the rename. But I would like to learn; I feel I am missing something important.

rename returns a CopySpec and Closures return the last value. So your constructor is configuring rename on the task and returning itself to be added to the list of things to copy (the from). Your file1 line is basically ignored and the return value from rename is used as the return value of the Closure to from(). I think you were trying to do ‘from(file1) { rename … }’.

I think this does what you wanted:

task myjar(type: Jar) {
  baseName 'foo'
  from("dir1/file1.txt") {
    rename { String fileName -> fileName.replace('file1.txt', 'file2.txt'); }
  }
}

Usually, you want to avoid extending tasks to add new behavior. If you can just configure it, that’s the best option.

Your suggestion solves the problem. When I change only the ‘from’ closure to

from(file1) {
  //file1;
  rename { String fileName -> fileName.replace('file1.txt', 'file2.txt'); }
}

then everything works as expected.

But I still don’t quite understand why that is. You said the ‘file1;’ line was “basically ignored”, and rename is still the last line in the closure, so how is the circular dependency avoided?

It has to do with which from method you’re calling on the Jar task (which implements CopySpec).

The first case is calling this method https://gradle.org/docs/current/javadoc/org/gradle/api/file/CopySpec.html#from(java.lang.Object...)

The second case is calling this method https://gradle.org/docs/current/javadoc/org/gradle/api/file/CopySpec.html#from(java.lang.Object,%20groovy.lang.Closure)

In the first case, the only parameter to from() is a Closure. That Closure gets called and the return value is the last value in the Closure (which is a reference to the outer task coming from rename()). It basically turns the call into from([this task]), that’s what causes the circular dependency. ‘file1’ is ignored because it’s not the last line in the Closure. I think it’s equivalent to something like this:

task myjar(type: Jar) {
   from(rename { String fileName -> fileName.replace('file1.txt', 'file2.txt') })
}

In the second case, the first parameter to from() is a File and the second is a Closure. The first parameter is resolved to a file (no problem). The second parameter is evaluated to configure the task (return value is ignored). Since you’re not adding the task to itself as an input (the from), you don’t get a circular dependency.

Thank you, this has been very helpful. Thanks for taking the time to explain!