Gradle: Copy selected files without using the exclude pattern
Copying files in Gradle is very intuitive. It’s easy enough to filter out unwanted files using the exclude pattern. Example (see documentation):
task copyTaskWithPatterns(type: Copy) { from 'src/main/webapp' into 'build/explodedWar' include '**/*.html' include '**/*.jsp' exclude { details -> details.file.name.endsWith('.html') && details.file.text.contains('staging') } }
The problem occurs, when you only want to copy specific files and nothing else. It’s not that the include / exclude patterns wouldn’t work, but they become tricky to use. Let’s start with creating a dummy project:
mkdir hello-gradle && cd $_ && gradle init --type java-library
Now add some resources to play with:
mkdir -p src/main/resources && dest=$_; for i in {1..9}; do touch $dest/file$i.txt; done
Result:
ls src/main/resources | xargs -n 1 basename file1.txt file2.txt file3.txt file4.txt file5.txt file6.txt file7.txt file8.txt file9.txt
Finally, let’s assume a hypothetical assets directory, which will be used as a target:
build/assets
Suppose that out of nine resources I only want the first, the third and the fifth file to be copied to the target. My initial instinct would be to only apply the include filter, see below.
build.gradle
... task copyFiles(type: Copy) { from 'src/main/resources' into 'build/assets' include '**/file1.txt' include '**/file3.txt' include '**/file5.txt' } processResources.dependsOn copyFiles
Now, run the build and check the result:
gradle build ... BUILD SUCCESSFUL ls build/assets | xargs -n 1 basename file1.txt file3.txt file5.txt
Wow, it works! So, what’s the fuss? Well, let me add a single subdirectory group1 containing a few dummy text files:
mkdir -p src/main/resources/group1 && dest=$_; for i in {11..13}; do touch $dest/file$i.txt; done
I leave the build script unchanged and rerun the build.
gradle clean build
Result:
tree build/assets file1.txt file3.txt file5.txt group1
As you can see, the group1 subdirectory hasn’t been filtered out. Not a big deal, since no unwanted files were copied. A simple tweak to my build script solves this minor annoyance:
task copyFiles(type: Copy) { ... exclude 'group1' }
However, imagine a more complicated structure. To illustrate the problem, let me add a few dependencies from another project and apply the same approach.
My resources after having added a bunch of JS libraries:
angular angular-mocks angular-route angular-sanitize jquery ... underscore
Let me amend the build script to only include the minified AngularJS library and nothing else:
task copyFiles(type: Copy) { from 'src/main/resources' into 'build/assets' include '**/angular.min.js' }
Here is the result of a new build:
tree build/assets ├── angular │ └── angular.min.js ├── angular-jqcloud │ └── examples ├── angular-mocks ├── angular-route ├── angular-sanitize ├── jqcloud2 │ ├── dist │ └── src ├── jquery │ ├── dist │ └── src │ ├── ajax │ │ └── var │ ├── attributes │ ├── core │ │ └── var │ ├── css │ │ └── var ...
Essentially, I got my file copied along with all surrounding subdirectories. Hope you start to realise that using the exclude pattern would be slightly more challenging at this stage. I tried a combination of include along with exclude all (**/*). Unfortunately, no files were copied as a result of that.
Also, note that the relative path to the file has been preserved. In fact, I might be more interested in simply putting the file directly to the root of the destination directory and not worry about the original structure.
Without further ado, here is how I solved the problem. The script below searches for the files identified by their name and places them to the provided location. Non-existent directories are created as needed.
// Populates a list of files def loadFiles(String... files) { files.collect({ file -> fileTree(dir:'src/main/resources') .matching{include '**/'+file} .singleFile }) } task copyFiles(type: Sync) { from loadFiles('angular.min.js', 'ng-tags-input.min.js', 'jquery.min.js') into 'build/assets/vendor/js' } processResources.dependsOn copyFiles
Once the build reruns, the output looks as follows (flat hierarchy and all files land exactly where I want them to be):
tree build/assets build/assets └── vendor └── js ├── angular.min.js ├── jquery.min.js └── ng-tags-input.min.js
Once again, this is the core function:
files.collect({ file -> fileTree(dir:'src/main/resources') .matching{include '**/'+file} .singleFile })
I find it elegant and powerful, really appreciate the flexibility of Gradle’s API when it comes to working with files.