Lazzy access of DefaultCopySpec vs. CopySpecWrapper

I want to understand the differences between DefaultCopySpec and CopySpecWrapper. The follow code has a different behavior depending of the parameter instance class.

ext.ikvm = { CopySpec sources ->
    def result = copySpec {
        sources.eachFile { FileCopyDetails details ->
            println "\tIKVM " + details.relativePath
        }
    }
    return result
}

With a DefaultCopySpec_Decorated it will be executed immediate and with a CopySpecWrapper_Decorated it will executed on first access in the execution phase. It is used in a zip task like:

zip {
    into( 'ikvm' ) with( ikvm( libs ) )
}

The problem with the immediate execution is that it will be execute on definition phase also if the task is not executed. If I do the declaration in a doFirst{} then the task is not executed because the zip is empty before. The result is a simple UP-TO-DATE. A workaround is a to use a task “beforeZip” and “zip” depends on it. But this is ugly for hundred of task.

But it work like expected if the “libs” parameter is a CopySpecWrapper_Decorated.

What is the difference between the both instances?
When I receive the one and one the other class?
Is there a secure solution to convert the one to another type?

This is an implementation Detail that shouldn’t make a difference while using Gradle. The only purpose of CopySpecWrapper is to hide some implementation details. From looking at the code project.copySpec always returns a DefaultCopySpec The snippet you listed above should definitely be evaluated lazy using default Gradle.

cheers,
René

Then this is a bug in Gradle?

Can you create a reproducable small example so I can verify? I don’t know what ‘libs’ is in your snippet and how it is created etc. Maybe it is related to the problem you’re describing here. So a small reproducible gist would be great.

cheers,
René

A simple sample is difficult because a small changes has different behavior. I also does not know if I receive a wrapper and when not. That my question here.

ext.ikvm = { CopySpec sources ->
    return copySpec {
        println "************** IKVM start"
        sources.eachFile { FileCopyDetails details ->
            println "************** IKVM " + details.relativePath
            it.include details.toString()
        }
        into 'ikvm'
    }
}
task sample( type: Zip ) {
    def libs = copySpec {
        from( "${buildDir}/libs" )
    }
    println libs.getClass()
    with libs
    with( ikvm( libs ) )
}

This produce the follow output:
************** IKVM start
:sample
************** IKVM pdfc-server-4.0-javadoc.jar
************** IKVM pdfc-server-4.0-sources.jar

The first println is executed in the definition phase. But the println in eachFile is evaluated on execution. If I remove the “width libs” line then the eachFile is never evaluated.

Ok, investigate more time I see now that I have "eachFile misunderstand completely. It is not an iterator over a copySpec. It is an callback event. Then the question is how can i iterate over a copySpec to create a second copySpec. And this lazzy.

I am here in 2022 wondering the same thing. Have you come up with anything @Horcrux7 ?

Can you specify what exactly you are wondering / what you want to do?
Because

Then the question is how can i iterate over a copySpec to create a second copySpec. And this lazzy.

I would interpret as

copySpec {
    with(sourceCopySpec)
    ...
}

We need a drop-in replacement to address the issues with the build-in Zip class. So we’ve modified zip4j and were hoping to write a custom task for it by extending the DefaultTask (Gradle API 7.6) but it’s quite challenging to deal with the pre-existing inputs specially the CopySpec.

I think I need a new subtype of AbstractArchiveTask (Gradle API 7.6) instead. Looks like getSource() in the AbstractArchiveTask can convert the CopySpec to a list of files and folders for us… I hope.

Yes, if you want to write an own zip-like task, I’d also create an AbstractArchiveTask subclass to get the common handling the same as for all other archive tasks.
I guess maybe the easiest is, if you look at the built-in Zip and / or Tar tasks and do your custom implementation after their example.

Unfortunately, we’re now running into this exception, the very thing we’ve locally fixed in zip4j. We run into this as a result of calling getSource().

Broken symbolic links are a reality that we cannot change (we must copy them), and we never want to follow links either, whether they are broken or not. I guess this goes back to Copy should copy symlinks, not resolve them · Issue #3982 · gradle/gradle · GitHub

Any advice on how to make the 2nd argument value of File.walkFileTree configurable (a way for users to tell the CopySpec to not follow links)?

No, I have no idea, sorry.