How to copy files verbosely using Project.copy()?

How to copy files in a more verbose mode?
I’m using Gradle’s Project#copy(Closure closure) and I need log output for each copied file. How can I do that?

A bit more in detail: I’ve written a Gradle plugin which implements a DSL and one of the DSL commands is to copy files. This copy method is not more than a proxy to Gradle’s Project#copy(Closure closure). While copying the files I’d like to get a log entry for each copied file.

I thought I could use Project#copySpec(Closure closure) to get the CopySpec and CopySpec#eachFile(Closure closure) but this doesn’t work.

Here’s my code:

    void copy(Closure closure) {
        // doesn't work
        project.copySpec(closure).eachFile { file ->
            log.info("copying $file...")
        }
        project.copy(closure)
    }

Any suggestions?

Have you tried something like:

void copy(Closure closure) {
   def copySpec = project.copySpec(closure)
   project.copy { 
      with copySpec
      eachFile { log.info(...) }
   }
}

In your example, you’re never using the CopySpec you create.

As an aside, I don’t know exactly what your plugin does, but it seems like you’re exposing too much of the internals to users because they are directly configuring a CopySpec. It’s usually much better to give domain-specific names to things for a user to configure vs giving a wide API and asking the user to handle everything. How is your DSL used?

Many thanks for your reply - this is exactly what I’m looking for. However, in my Gradle 2.8 environment this code throws an NPE, even in an isolated simple Gradle script, even without the ‘eachFile { }’ closure. Here’s the stacktrace: stacktrace.zip (2.0 KB)

Regarding your question: My plugin implements a DSL with functionalities for an automated install/update/deploy/modify of server environments. Before, we had many manual steps for updating individual server environments, like copying / deleting files, modifying configuration files, uninstalling / installing Windows service, executing SQL scripts etc. The plugin is used in Gradle scripts which are developed by a handful software engineers in our (small) company. As copying and deleting files is available in any Gradle script, I believe there’s nothing bad to have this functionality “as is” in the plugin DSL too.

Just to give you an idea, a simple script would look as follows:

apply plugin: 'com.company.product-update'
...

ProductUpdate {
    process {
        // parse config files, setup context information (done during plugin startup)
        ...
        stopServer()
        uninstallService()
        delete( fileTree(
            dir: "${ext.productRootDir}/conf/",
            excludes: [ **/*.lic, **/*.server.xml ]
        ))
        copy {
            from "${ext.sourceRootDir}/conf/"
            into "${ext.productRootDir}/conf/"
            //... filters, excludes, ...
        }
        executeSQL( "${ext.sourceRootDir}/SQL scripts/*.sql" )
        installService()
        startServer()
    }
}

Sterling:

Could you elaborate a bit about the DSL vs API point? I think I understand the point, but would love clarification if you don’t mind.

If you consider something like ExecSpec, if you were building a DSL to run a particular tool, you could expose all of the ExecSpec API or you could provide something more domain-specific.

So if you had a tool that’s command line looked like:

./tool -h
  -o outputDir
  -i inputDir
  -v verbose mode
  -f cool flag

You could have an Exec task that your users had to setup (easy to get wrong):

task execTool(type: Exec) {
  executable 'tool'
  args '-o', file("output"), '-i', file("input")
}

And you could make it “more convenient” with a method, which expands to about the same thing (complexity multiplies as you add options):

plugin.execTool(file("output"), file("input"), false, false)

Or you could make a custom Exec-like task (usual go-to choice):

task execTool(type: CustomTask) {
   output = file("output")
   input = file("input")
}

And to wrap it up, if you just wanted to make it easier to add exec actions to an existing task, you might have a builder DSL (this makes sense if you’re composing lots of little things that don’t make sense as separate tasks, but you have to be careful that you’re not missing inputs/outputs for the overall task):

task someTask {
  doLast plugin.execTool().output("output").input("input").build()
}

The central idea is to expose something that you can document and test vs giving unlimited freedom (or at least putting the unlimited freedom behind an escape hatch). So, if you say “ExecSpec” is my API and DSL, you’re stuck with whatever that means, even if you decide later that you need to inspect/restrict the things someone can do with that interface. And your users are stuck with having to understand how to map the generic interface to a specific domain.

My suspicion is that whatever underlying copy you’re doing, you haven’t specified a base directory to copy into.

This recreates a similar NPE for me:

def cs = project.copySpec {
  from "here"
  into "there"
}

project.copy {
  with cs
}

The project.copy{} needs an into that tells us where all the other into’s will be relative to. I think there’s a bug that we don’t fail with a nicer error.

I’m assuming that ProductUpdate/process eventually turns into a task and these are all just actions for that task? Does it ever make sense for any of these steps to be incremental or is the task just set to always run?

Many thanks for taking the time to respond Sterling. This made a lot of sense.