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])
}
}
} }