Combining Tasks with Grunt
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.
tasks/drush.js
module.exports = function(grunt) {
/**
* Define "drush" tasks.
*
* grunt drush:make
* Builds the Drush make file to the build/html directory.
*/
grunt.loadNpmTasks('grunt-drush');
grunt.config('drush', {
make: {
args: ['make', '<%= config.srcPaths.make %>'],
dest: '<%= config.buildPaths.html %>'
}
});
};
You can see that the task contains a few instances of variable interpolation, such as </code>. By convention, the values of these variables go in a file called <code>Gruntconfig.json
and are set using the grunt.initConfig
method. In addition, the configuration for the default task lives in a file called Gruntfile.js
. I have put trimmed examples of each below.
Gruntfile.js
module.exports = function(grunt) {
// Initialize global configuration variables.
var config = grunt.file.readJSON('Gruntconfig.json');
grunt.initConfig({
config: config
});
// Load all included tasks.
grunt.loadTasks(__dirname + '/tasks');
// Define the default task to fully build and configure the project.
var tasksDefault = [
'clean:default',
'mkdir:init',
'drush:make'
];
grunt.registerTask('default', tasksDefault);
};
Gruntconfig.json
{
"srcPaths": {
"make": "src/project.make"
},
"buildPaths": {
"build": "build",
"html": "build/html"
}
}
As you can see, the project’s Gruntfile.js
also has a clean:default
task to remove the built site and a mkdir:init
task 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.
build.xml
<target name="-drush-make-uptodate" depends="init" hidden="true">
<if>
<available file="${html}/index.php" />
<then>
<uptodate property="drush.makefile.uptodate"
targetfile="${html}/index.php" srcfile="${drush.makefile}" />
</then>
</if>
</target>
<!-- Use drush make to build (or rebuild) the docroot -->
<target name="drush-make" depends="-drush-make-uptodate, init"
hidden="true" unless="drush.makefile.uptodate">
<if>
<available file="${html}"/>
<then>
<echo level="info" message="Rebuilding ${html}."/>
<delete dir="${html}" failonerror="true"/>
</then>
</if>
<exec executable="drush" checkreturn="true" passthru="true" level="info">
<arg value="make"/>
<arg value="${drush.makefile}"/>
<arg value="${html}"/>
</exec>
</target>
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 build.properties
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:make
task.
grunt.config('drush', {
make: {
args: ['make', '<%= config.srcPaths.make %>'],
src: '<%= config.srcPaths.make %>',
dest: '<%= config.buildPaths.html %>'
}
});
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 grunt.task.run
that combined the three tasks existing tasks and then use the grunt-newer
version of that task. The solution looked much like the following.
tasks/drushmake.js
module.exports = function(grunt) {
/**
* Define "drushmake" tasks.
*
* grunt drushmake
* Remove the existing site directory, make it again, and run Drush make.
*/
grunt.registerTask('drushmake', 'Erase the site and run Drush make.', function() {
grunt.task.run('clean:default', 'mkdir:init', 'drush:make');
});
grunt.config('drushmake', {
default : {
// Add src and dest attributes for grunt-newer.
src: '<%= config.srcPaths.make %>',
dest: '<%= config.buildPaths.html %>'
}
});
}
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.”