Altering copy tasks during execution

I’ve run into scenarios several times in which I want to alter the way files are mapped by the CopyTask based on the structure of the rest of the destination file tree. This is a tricky problem to solve given the nature of CopyTasks. I thought I’d share the latest scenario, and the solution I’m using to workaround it.

I have a plugin which adds a VERSION file to any archive task. I recently wanted to modify the plugin such that if the resulting archive has only a single root folder and no root files, put the VERSION file under that one folder with everything else. Otherwise, just put the VERSION file in the root of the archive. For example, if the archive looks like this:

mytool.tar

bin/

libs/

docs/

…then add VERSION to the root, like so:

mytool.tar

bin/

libs/

docs/

VERSION

But if the tar is packaged like many distributions, with a single folder at the root of the archive:

mytool.tar

mytool/

bin/

lib/

docs/

…then I want the VERSION file under that top level folder:

mytool.tar

mytool/

bin/

lib/

docs/

VERSION

I somehow needed my plugin to add a file to all archive tasks, but put the file in a destination dependent on what the rest of the archive ends up looking like. I haven’t figured out an obvious way to do this in gradle.

The workaround I came up with uses eachFile() to visit the destination files, but uses a little bit of a hack to ensure that my VERSION file is the last file visited, giving me the opportunity to move it based on what I’ve observed of the rest of the files. Here’s the code. Note that “versionFile” is another task I wrote which builds the VERSION file.

project.afterEvaluate {

// Make sure all archives contain a VERSION file

tasks.withType(AbstractArchiveTask) { task ->

task.dependsOn(versionFile)

// this is a hack. This whole thing only works if the VERSION file

// visited last by the eachFile() call, and that will only happen

// if its CopySpec is added as the last child of the root CopySpec

// if I don’t add the closure argument (the {}), the VERSION file is

// added as a source path to the root CopySpec, to a Set of sources.

// So there is no guarantee of the order in which those sources will

// be visited. If I add the closure argument, then the VERSION file

// is added as a full child CopySpec, to a list of children, where

// order is guaranteed, and I know this will get visited last.

task.from(versionFile.versionFile.path) {}

// jars are a special case, usually have a single root folder, but are

// never unzipped, so I still want the VERSION file in the root

// of the archive

if(task.extension != “jar”) {

def rootFileCount = 0;

def rootDirs = [] as Set

// visit all the files going into the archive, and take note

// of the first directory on their path

// if there end up being no files in the root of the archive,

// and only one folder in the root of the archive, then when

// I finally visit the VERSION file, move it into that one lone

// root folder

eachFile { FileCopyDetails details ->

if (details.name != versionFile.versionFile.name)

switch (details.relativePath.segments.length) {

case 1:

// Need to handle an edge case of an archive

// containing only a single file at its root

rootFileCount++

break

default:

rootDirs.add(details.relativePath.segments[0])

break

}

else

if (!rootFileCount && rootDirs.size() == 1)

details.relativePath = details.relativePath.prepend(rootDirs.asList()[0])

}

}

} }

I spoke too soon. While this does work in most cases, it doesn’t work if an CopySpecs have been added to the task using the with() method. These CopySpecs are ignored by eachFile(), so you’ll never see the files coming from them. Noticed my code wasn’t working with the zips created by the Application plugin.

It seems that with(CopySpec) wraps the arg in a WrapperCopySpec. This differs from a CopySpecImpl is that getAllCopyActions() doesn’t return actions on the parent specs of the ‘root’ spec. So any eachFile() copy actions you append to a Copy task don’t get inherited by any WrapperCopySpec instances. Not sure if this is intentional.

Given that, I think I just can’t do this.

it doesn’t work if an CopySpecs have been added to the task using the with() method. These CopySpecs are ignored by eachFile()

Verified. This looks like a bug to me.

Ah, ok. I was about to look at your test cases to see if it was intentional.

I’ve created GRADLE-2112 for this.