Combining Tasks with Grunt

Combining Tasks with Grunt

Steven Merrill, Director of Devops
#Development | Posted

I was recently asked to help out with a few build steps for a Drupal project using Grunt as its build system. The project's Gruntfile.js has a drush:make task that utilizes the grunt-drush package to run Drush make. This task in included in a file under the tasks directory in the main repository.


  1. [geshifilter-code]module.exports = function(grunt) {
  2. /**
  3. * Define "drush" tasks.
  4. *
  5. * grunt drush:make
  6. * Builds the Drush make file to the build/html directory.
  7. */
  8. grunt.loadNpmTasks('grunt-drush');
  9. grunt.config('drush', {
  10. make: {
  11. args: ['make', '<%= config.srcPaths.make %>'],
  12. dest: '<%= config.buildPaths.html %>'
  13. }
  14. });
  15. };[/geshifilter-code]

You can see that the task contains a few instances of variable interpolation, such as <%= config.srcPaths.make %>. By convention, the values of these variables go in a file called Gruntconfig.json and are set using the grunt.initConfigmethod. In addition, the configuration for the default task lives in a file called Gruntfile.js. I have put trimmed examples of each below.


  1. [geshifilter-code]module.exports = function(grunt) {
  2. // Initialize global configuration variables.
  3. var config = grunt.file.readJSON('Gruntconfig.json');
  4. grunt.initConfig({
  5. config: config
  6. });
  8. // Load all included tasks.
  9. grunt.loadTasks(__dirname + '/tasks');
  11. // Define the default task to fully build and configure the project.
  12. var tasksDefault = [
  13. 'clean:default',
  14. 'mkdir:init',
  15. 'drush:make'
  16. ];
  17. grunt.registerTask('default', tasksDefault);
  18. };[/geshifilter-code]


  1. [geshifilter-code]{
  2. "srcPaths": {
  3. "make": "src/project.make"
  4. },
  5. "buildPaths": {
  6. "build": "build",
  7. "html": "build/html"
  8. }
  9. }[/geshifilter-code]

As you can see, the project's Gruntfile.js also has a clean:default task to remove the built site and a mkdir:inittask to make the build/html directory, and the three tasks are combined with grunt.registerTask to make the default task which will be run when you invoke grunt with no arguments.

A small change

In Phase2's build setup using Phing we have a task that will run drush make when the Makefile's modified time is newer than the built site. This allows a user to invoke the build tool and only spend the time doing a drush make if the Makefile has indeed changed.

The setup needed to do this in Phing is configured in XML: if an index.php file exists and it is newer than the Makefile, don't run drush make. Otherwise, delete the built site and run drush make. The necessary configuration to do this in a Phing build.xml is below.


  1. [geshifilter-code]
  3. <!-- Use drush make to build (or rebuild) the docroot -->
  5. [/geshifilter-code]

You'll note that Phing also uses variable interpolation. The syntax, ${html}, is similar to regular PHP string interpolation. By convention, parameters for a Phing build live in a file.

A newer grunt

The grunt-newer plugin

 appears to be the proper way to handle this. It creates a new task prefixed with newer: to any other defined tasks. If your task has a src and dest parameter, it will check that src is newer than dest before running the task.

In my first quick testing, I added a spurious src parameter to the drush:make task and then invoked the newer:drush:maketask.

  1. [geshifilter-code]grunt.config('drush', {
  2. make: {
  3. args: ['make', '<%= config.srcPaths.make %>'],
  4. src: '<%= config.srcPaths.make %>',
  5. dest: '<%= config.buildPaths.html %>'
  6. }
  7. });[/geshifilter-code]

That modification worked properly in concert with grunt-newer (and the drush task from grunt-drush task didn't complain about the extra src parameter,) but I still also needed to conditionally run the clean:default and mkdir:init only if the Makefile was newer than the built site.

Synchronized grunting

The answer turned out to be to create a composite task using grunt.registerTask and that combined the three tasks existing tasks and then use the grunt-newerversion of that task. The solution looked much like the following.


  1. [geshifilter-code]module.exports = function(grunt) {
  2. /**
  3. * Define "drushmake" tasks.
  4. *
  5. * grunt drushmake
  6. * Remove the existing site directory, make it again, and run Drush make.
  7. */
  8. grunt.registerTask('drushmake', 'Erase the site and run Drush make.', function() {
  9.'clean:default', 'mkdir:init', 'drush:make');
  10. });
  11. grunt.config('drushmake', {
  12. default : {
  13. // Add src and dest attributes for grunt-newer.
  14. src: '<%= config.srcPaths.make %>',
  15. dest: '<%= config.buildPaths.html %>'
  16. }
  17. });
  18. }[/geshifilter-code]

I could then invoke newer:drushmake:default in my Gruntfile.js and only delete and rebuild the site when there were changes to the Makefile.

Learn more about build systems in Adam Ross's blog post "Creating Options in Automated Software Deployment."

Steven Merrill

Director of Devops