How to use grunt in your maven lifecycle

Maven is the build lifecycle tool for your java projects; grunt is the build lifecycle tool for your front end projects. Can they coexist, and if so how?

Unless you are using groovy in which case you are most likely using gradle, Maven (mvn) is the defacto build tool for java projects. It’s mature and well supported and all of the mainstream Continuous Integration (CI) stacks have integrations for it. It manages your dependencies, compiles your code, runs your tests, packages your code, and deploys built artefacts to your repositories. In respect of web applications, it works really well for REST API applications (where the java code is serving the RESTful API) and for traditional web applications where the views are managed and served from the java server.

grunt is a build tool for building modern front-end applications. Conceptually similar to mvn, it can execute tasks to perform all manner of build related chores such as concatenating and minifying your code, running your tests etc. There are other similar tools such as gulp, brocolli, and others; but this post concentrates on grunt. If you are using other front-end build tools the concepts described should still apply, and hopefully you will see what to change and be able to get it working.
By modern front-end applications I mean web applications that are written in javascript. The front-end will contain a lot of logic, will be managing the views, and perhaps be managing state through scopes, browser storage or similar techniques. The modern front-end application will typically make http requests to a RESTful server.

Why would a project need both?

If your project is purely javascript – perhaps an angular front-end with a node back-end – the chances are you have no java in your infrastructure and will be doing everything in a build tool such as grunt. If this is you, you are probably reading the wrong post (but you’re more than welcome here anyway 🙂 )

Even if your web application is more traditional with it’s views composed using server side view templating technologies such as jstl, thymeleaf, freemarker etc; you probably have some front end code that needs some form of build process. Maybe you are using javascript to give some form of UX enhancement – jquery and it’s plethora of plugins springs to mind – and you need to concatenate and minify the code. Or perhaps your css is authored in sass or less, and you have a compilation phase to compile down to regular css. It is likely you will have a solid mvn build process, and the front-end might have a small grunt script but likely won’t be integrated into your mvn project build and CI stack. Maybe kicking off the grunt script to compile the css is a manual task. The front-end is often seen as the poorer cousin, and, well, who needs to worry too much about unit testing a few jquery plugins? (that’s not our view by the way!)

Increasingly often we are now seeing projects where the back-end is written as a pure RESTful API in java technologies, and the front-end is architected as a javascript single page application, perhaps in angularjs. Both sides of the code (back-end and front-end) might be in the same codebase and form part of the same deliverable. As you have always done, the java code is well managed with a mvn build. But now the front-end code is much more important. You might have a css compilation phase, but more importantly, the client side code is now much more of an application. It is designed much more like how we used to write web applications in java with views, models, controllers and services etc. You have much more javascript code that will need concatenating and minifying as part of packaging, and of course, you now have a thorough suite of tests to test the javascript code. And because you’ve used modern coding for the front-end, you’re using a modern build tool such as grunt.

Having a mvn build process looking after the java code is great, as is having a grunt script to manage the build of the front-end code. But they are separate and disconnected. You are building an application that spans the back-end and front-end code but in different technologies, and you need a single build process that will build and test all artefacts of the code base.

The Approach

The approach is to use one of the build tools to perform it’s normal functions, and to then call out to the other. You could either use grunt as your ‘master’ with it calling out to mvn, or the other way round. Within Clarity projects we’ve not had the need to use grunt as the master, but we see no reason why you shouldn’t be able to get this working with something like grunt-shell to shell out to mvn, and using a simple shell script to get your CI stack to run your grunt script.

But this post is about describing our experience of how to get mvn to be the master, calling out to grunt; so let’s crack on.

mvn as the master

In a nutshell, to get this working, you need to hook into one or more mvn lifecycle phases to shell out and execute your grunt tasks. The mvn exec plugin might sound like it’s a good tool for this job, but we’ve found it not flexible enough for our needs in this case. Instead the mvn ant plugin offers some more flexibility in it’s configuration.

On the surface of it, the ant plugin sounds like something you don’t want in your mvn build. ant is no longer a popular java build tool (as once it was), and why would you want two java build tools managing your project? But if you’re careful about what you use it for and keep the configuration in your pom.xml rather than build.xml (yes, really), it can be a great solution.

The mvn ant plugin allows you to execute ant scripts and even, as is the case here, embed ant scripts within your pom.xml. Whilst not popular today, ant is quite powerful having features such as rudimentary branching and flow control within your script. This is something we can take advantage of as shown in the pom.xml below. This type of flow control is quite hard using the mvn exec plugin and is one of the reasons why we favour the ant plugin.

The pom.xml

...
<build>
      <plugins>

         <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>1.7</version>
            <executions>
               <execution>
                  <id>installNpm</id>
                  <phase>initialize</phase>
                  <configuration>
                     <target name="Installing npm etc">
                        <echo>
                           Installing npm modules
                        </echo>
                        <exec executable="cmd" dir="${project.basedir}" osfamily="windows" failonerror="true">
                           <arg line="/c npm install -f"/>
                        </exec>
                        <exec executable="npm" dir="${project.basedir}" osfamily="unix" failonerror="true">
                           <arg line="install"/>
                           <arg line="-f" />
                        </exec>
                        <echo>
                           Installing bower components
                        </echo>
                        <exec executable="cmd" dir="${project.basedir}" osfamily="windows" failonerror="true">
                           <arg line="/c bower install -f"/>
                        </exec>
                        <exec executable="bower" dir="${project.basedir}" osfamily="unix" failonerror="true">
                           <arg line="install"/>
                           <arg line="-f" />
                        </exec>
                     </target>
                  </configuration>
                  <goals>
                     <goal>run</goal>
                  </goals>
               </execution>

               <execution>
                  <id>runJsTests</id>
                  <phase>test</phase>
                  <configuration>
                     <target name="Running js tests">
                        <echo>
                           Running js tests
                        </echo>
                        <exec executable="cmd" dir="${project.basedir}" osfamily="windows" failonerror="true">
                           <arg line="/c grunt test"/>
                        </exec>
                        <exec executable="grunt" dir="${project.basedir}" osfamily="unix" failonerror="true">
                           <arg line="test"/>
                        </exec>
                     </target>
                  </configuration>
                  <goals>
                     <goal>run</goal>
                  </goals>
               </execution>
             </executions>

          </plugin>
        </plugins> 
      </build>
</project> 

The above pom.xml hooks into two mvn lifecycle phases: initialize (lines 9 thru 39) and test (lines 41 thru 61)

The initialize phase

This phase happens early in the mvn lifecycle (for reference here are the mvn lifecycles) and you can hook into it to install the required modules and packages from your npm package.json; and also install any other components such as bower dependencies.

Windows or linux/osx

You will notice that we appear to be doing each step twice. npm modules are installed on lines 17 thru 23, and the bower dependencies are installed on lines 27 thru 33. There are a pair of exec elements which look like they do the same thing. The way we execute commands on windows and linux/osx is different. With windows we execute the command interpreter cmd with the /c parameter specifying the command to run. On linux/osx platforms we can simply execute the command we want to run. The ant exec command provides an osfamily attribute to target the different operating systems. This kind of rudimentary branching is one of the flexible features that the ant plugin gives us.

The test phase

The second mvn phase we hook into is the test phase. Using the same technique of a pair of exec elements, we execute grunt with the task ‘test’. This means that during the mvn test phase, where it would run your java unit tests, it will also run your grunt test task to run your javascript tests.

In all cases we use the failonerror attribute of the exec element. This means that if the command that ant is executing (npm, bower or grunt) fails, it will fail the whole mvn build.

Summary

The mvn lifecycle provides a number of convenient hooks from which you can use the ant plugin to call out to grunt and other modern build tools as part of your build. You can use this technique to not only run your javascript tests in the test phase, but also to run other grunt tasks such as compiling css and minifying code during the package phase. Even though your project may use different technologies, you can create a unified build process for all components. And this approach works really well with your Continuous Integration stack as a failure of any ant exec element will cause the whole mvn build to fail. You will have a single build process for your whole project, with any failing javascript tests causing a failure of your whole mvn build.

 

Leave a Reply

Your email address will not be published. Required fields are marked *