|

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.

Similar Posts