JavaScript Webapps with Gradle
Gradle is a build tool on the JVM platform that’s been gaining prominence over the last few years. Gradle’s reach doesn’t stop with JVM languages though. The most recent releases, 1.10 and 1.11, have improved support for compiling native code (C / C++ / Objective C) and introduced support for the .NET platform. For Android projects, Google have made Gradle the de facto build tool. It’s getting a name for its flexibility, so when the opportunity came to build a single page Javascript webapp we decided to put the latest addition to the polyglot build tool arsenal through its paces. In this post I’m going to look at why we chose Gradle over a native JS build tool, provide a quick tour of the JS/CSS plugins, look at the embedded Jetty webserver plugin, discuss testing your code in CI, and a variety of other tips and gotchyas found in our experience during the project. Why not {{JavaScript_build_tool_flavour_of_the_month}? The momentum around JavaScript development is growing rapidly. So rapidly in fact that it feels like the book 1984 where one day your nation is fighting a war with one country, then all of a sudden wakes up, and it’s announced it has pledged an allegiance to a former enemy; your former ally becoming the new enemy. Twitter and the blogosphere recently became alive with talk about Gulp, a new JS build tool. This blog post ‘And just like that, Grunt and RequireJS are out – its all about Gulp and Browsify now’ and this Twitter post felt like the shifting of alliances in 1984. This is something you’ll have to factor into your JS build tool decision making. It also means however there is still room for contenders in this space. Having the same build tool for both client side and server side builds helps developers understand the project better. Our team had Groovy and Gradle exposure already and whilst we were happy to try a JS build tool, it would be nice to consider the same tools for both aspects of the project. A tool that works with your existing infrastructure is also important. The straw that broke the camel’s back was our client’s firewall. Firewalls are part and parcel of something we have to work around as consultants. Sometimes you win, sometimes you lose. Simply put, our client’s firewall made npm cry as the https proxy had some confusing message about SSL certificate algorithms. After trying to work around this for a while, I noticed we had a perfectly fine build tool that already worked fine with Java and our proxy. The question was asked: Are there any JavaScript plugins for Gradle? Reinventing the wheel I heard at a JavaScript BOF at JavaOne someone remark that ‘JavaScript had been rapidly climbing the tree of software development, reinventing tools that had already existed on established platforms, then falling off the tree taking each branch along with it on the way down.’ Although tongue-in-cheek, it did capture the frustrations of tools already built that were largely ignored, or unknown by the JavaScript community. From what I’ve seen so far, the other popular JVM build tool, Maven, has a large number of plugins to aid in the preparation of JavaScript files. Most of these use the Rhino JavaScript Engine that comes with Java in order to execute these utilities on the JVM platform where the build and testing is performed. The eco-system of running JavaScript utilities out of Rhino with ScriptEngineManager is well established. Additionally there are a lot of other tools written in Java to validate and compress JavaScript files – the Google Closure Compiler is one that is used by the Gradle JS and CSS plugin. It appears to have been relatively straight forward for plugin devs to repackage all these tools as Gradle plugins that kick off the same scripts from within. Where there isn’t a Java port or Jar, Gradle plugins or tasks can always launch JS tools from the command line. Kicking the tires So just what can Gradle with the available JS plugins do? Here’s a whirlwind tour of the available features. The Gradle JS and CSS plugins The Gradle JS plugin is a third party Gradle plugin written by Eric Wendelin. It can do file combination, minification, Gzip, JSHint and more. And although we didn’t need it for this project, you’ll be pleased to know it has require.js support as well. The Gradle CSS Plugin also written by Eric Wendelin, again supports combining and minification plus supports less compilation and csslint. To add these plugins to your build and associated tasks, all you have to do is add this to your build.gradle apply plugin: 'js' apply plugin: 'css' // define the dependencies gradle buildscript will use to build // the app (not the app itself) buildscript { repositories { mavenLocal() mavenCentral() } dependencies { classpath 'com.eriwen:gradle-css-plugin:1.8.0' classpath 'com.eriwen:gradle-js-plugin:1.9.0' } } You can then configure the tasks the plugin provides, combineJs, minifyJs, gzipJs as follows: combineJs { // pull together the source from string & file lists // eg. def core = ["$webAppDirName/init.js",...] source = core + application + devmode + bigquery + javascript.source.externalLibs.js.files + dfpFilters + chartdef + uxFilters // show the resolved files when gradle is run with -d source.each{ logger.debug ("$it") } dest = file("${buildDir}/all.js") } minifyJs { source = combineJs dest = file("${buildDir}/all.min.js") sourceMap = file("${buildDir}/all.sourcemap.json") closure { warningLevel = 'QUIET' compilerOptions.defineReplacements = ['MY_DBUG_FLAG':false] } } gzipJs { source = minifyJs.dest dest = file("${buildDir}/all.min.js.gz") } combineJs has a source property which takes a collection. We split our app into groups of JavaScript files and combine them with a list of files from a Gradle javascript sourceset called externLibs that references bootstrap and jquery in a separate dir in our project. The combine is done in the order you specify when specified in an ArrayList. For Gradle aficionados, this is why I’m not using sourcesets for our files – see here. The other reason is that all our client JS was in the same folder so lists were the most concise way to declare these file groups. You can see we can also debug the list to make sure we’ve pulled in the files we think we’ve asked for by using standard groovy code to iterate through the source collection, making use of the logger that is injected into the build file by Gradle. One of the neatest things about Gradle is that each task has a set of Input files or dirs, plus output files. Gradle can do some very intelligent introspection into whether or not a file has been modified and what dependent tasks need to be rerun. Spending a little effort to specify these properly means that your build won’t have to do unnecessary stuff repeatedly. The minifyJs task takes advantage of this. We can then declare the input for its source property as the dest outputfrom the previous task. Here we set source to combineJs and gradle figures out we mean the output of that task (we could have also said combineJs.dest). The cool thing about this is that Gradle will be lazy. If it notices that the output on the combineJs hasn’t changed, thus the input on minifyJs hasn’t changed, it will skip running that task. BTW, you can always force Gradle to run each task with the --rerun-tasks command line parameter. To help things along we can define the dependency chain on these tasks tasks.minifyJs.dependsOn tasks.combineJs tasks.gzipJs.dependsOn tasks.minifyJs and run gzipJs each time being confident it will only run the earlier tasks if their task inputs have changed. I’m not sure why the dependency chain isn’t configured by default in the JS plugin – perhaps it could be a reasonable enhancement for a future release? I mentioned that minifyJs uses the Google Closure Compiler under the hood and so I wanted to show our use of configuring it with closure config block, in particular the compilerOptions.defineReplacements option which wasn’t documented in the JS plugins docs. In our JS code we define a constant to indicate some developer mode functionality like so: // config.js /** * Flag to indicate console and extra logging throughout the app * @define {boolean} allow the value of this bool to be * overwritten at closure compiler / minification time * @const * @type {boolean} */ var MY_DBUG_FLAG = true; Then elsewhere throughout our code base, we have code like this which modifies the DOM and adds some helpful info about the applications state to the UI. if (MY_DBUG_FLAG) { $('.dashboard').append( 'Here's some dev info you wouldn't normally see' ); } defineReplacements allows the Google Closure compiler to redefine the MY_DBUG_FLAG constant at build time to false, and then through its optimisation, remove the if (MY_DBUG_FLAG) blocks from the resultant all.min.js entirely (since we’ve forced MY_DBUG_FLAG to false). There are plenty of other configuration options for the compiler but it’s not immediately apparent where these are set. Take a look at CompilerOptions.java in the Google Closure compiler source to see the properties it takes. CSS The CSS plugin adds combineCss, minifyCss, and gzipCss tasks. Here’s the config for the plugin & how we configured it. String[] cssDirs = ["src/main/webapp/$bootstrapPath/css", "src/main/webapp/libs/bootstrap-typeahead/0.9.3/css", "src/main/webapp/libs/bootstrap-datepicker/1.2.0/css", "src/main/webapp/$chosenPath", "src/main/webapp/$ajaxLoaderPath", "src/main/webapp/styles"] css.source { dev { css { srcDirs cssDirs include "*.css" include "*.min.css" exclude ".#*.css" // VCS creates these annoying files } } } Here’s another example of using the Gradle sourcesets, we have a list of our dependent CSS libs + our own CSS, using srcDirs to pull together these directories and let the source set definition define what files to include and exclude. Then pass it off to a combineCss source property. combineCss { source = css.source.dev.css.files dest = "${buildDir}/all.css" } combineCss << { source.each { logger.info "Combining $it"} } A small change made in this example is to add a doLast (aliased <<) closure on the combineCss task to do the non-essential logging of the files found after the task has been configured and actioned. I think it helps keep the build file a bit cleaner. The other thing you probably have noticed is the interleaving of properties such as ${buildDir}, $bootstrapPath, $webAppDirName. This is just Groovy syntax to refer to variables. Gradle does some binding magic to bind those variables to properties, either defined in the plugins, or within whatever you put in your gradle.properties file that lives in the same directory as build.gradle. # Path to Javascript files jsSrcDir=src/main/webapp/js # Path to json files used to describe UI controls jsonPath=src/main/webapp/json # Local Deployment wwwPath=/var/www openBrowserPostLocalDeploy=true # Library Paths jqueryPath=libs/jquery/2.0.3 ajaxLoaderPath=libs/ajaxloader/1.0 chosenPath=libs/chosen/1.0.0 bootstrapPath=libs/bootstrap/3.0.2 Just for completeness here are the minify and gzipCss tasks but now you are down with Gradle and Js, the below should be self-explanatory. minifyCss { source = combineCss dest = "${buildDir}/all.min.css" yuicompressor { // Optional lineBreakPos = -1 } } gzipCss { source = minifyCss dest = "${buildDir}/all.min.css.gz" } Deploy against the machine So we got to generating some minified, gzipped files. Great. Now we need to see our changes with an apache or nginx install. It’s a relatively short task to install through a couple of sudo apt-get commands on Ubuntu, or re-enabling web-sharing on OS X (google it) but admittedly a few hoops need to be jumped through. You also need to set some file permissions so you can let your build tool write to Apache’s /var/www dir. Once the formalities are out of the way though we can automate this deploy. Like any decent build tool, Gradle itself comes with tasks to copy, archive and manipulate files. Gradle copy tasks take a copy configuration closure which let you define properties on which file patterns you wish to include, exclude and which dirs (much like the source sets shown above). The copy tasks are also smart enough to know what’s already been copied and only copied changed files which can save a bit of time when it comes to deploying. Here are tasks for copying the all.min.js and css (plus gzipped versions and sourcemaps) task deployCombinedJsLocally(dependsOn: 'gzipJs', type:Copy) { from files(minifyJs.dest, minifyJs.sourceMap, gzipJs.dest) into wwwPath } task deployCombinedCssLocally(dependsOn: 'gzipCss', type: Copy) { doFirst { logger.quiet("Copying ${files(minifyCss.dest, gzipCss.dest)} ... to ${wwwPath}/css") } from files(minifyCss.dest, gzipCss.dest) into file("$wwwPath/css") } For the css task, I wanted to show an example of logging the files that would be copied but this time using Gradle’s default quiet log level so it would be printed out without needing to specify this flag. Also this logging happens within a doFirst closure block which will happen during the evaluation stage of the gradle build before the task executes. It would also be nice to also have a task to copy my un-minified files to the server: task deployJsLocally(type: Copy) { from files(javascript.source.pcr.js.files) into "$wwwPath/js" } task deployCssLocally(type: Copy) { from css.source.dev.css.files into "$wwwPath/css" } Finally we’d have a deploy task that referenced these and some copy tasks to pull in images, html and other non-css / js artefacts. task deployLocalMinified( description: 'Copy webapp artifacts to local apache', dependsOn: ['copyBootstrapFonts', 'copyJson', 'copyImages', 'addDevBootLibs', 'copyAllHtml', 'deployCombinedJsLocally', 'deployCombinedCssLocally'] ) {} An embedded web server Using a local Apache instance was all good and well, but as I mentioned it did require a developer to sort out their own installation. Gradle comes with a Jetty plugin out of the box and so we utilised this to provide a webserver which served our single page webapp. There was no need to provide a WEB-INF/web.xml, the default Jetty configuration is already configured to look in your webAppDir(src/main/webapp) and serve resources from that. Because it’s running in Jetty and out of the source folder as well, we could change the code out of our IDEs and have it update on the next reload – much better than having a grunt task to keep your source folders and apache in sync. apply plugin: 'jetty' // PROPERTIES (Common to jettyRun/RunWar/Stop tasks) // stop a running jetty stopPort = jettyStopPort as Integer stopKey = jettyStopKey jettyRun.doFirst { logger.quiet('Starting Jetty...') } jettyRun { scanIntervalSeconds = 5 contextPath = '/my-web-app' } IntelliJ has a neat feature that comes from its Webstorm IDE. If you install the LiveEdit plugin and the corresponding Chrome or Firefox extension, your code will update in the IDE whilst you are editing it. This saves a lot of time. You also get access to the debugging tools in IntelliJ as opposed to Chrome Dev Tools. The only tricky thing about setting this up was telling the Debug task in IntelliJ that http://localhost:8080/my-web-app/ was equal to /src/main/webapp so that breakpoints would work. Now any developer can simply check out our project from source control and start running the app by executing gradle jettyRun.It’s an important win – hence I’ve highlighted it. Multiple build files At this stage our build only looks after 3 concerns (CSS build, JS build and deployment) but our build.gradle file is starting to become a bit long. Gradle supports multiple build files so we can organise each concern into a separate build.gradle file to avoid clutter. Each buildscript has its own scope, applying its own plugins. That means that variables, methods and tasks that you or your plugins add are only in the scope of each child file. There isn’t a chance of other plugins polluting the namespace and causing issues you have to spend a lot of time to troubleshoot. They still have access to the project variable added by gradle to get access to configurations in other files if need be. We can define dependencies between our JS and CSS tasks. Our main build.gradle is now very short, with some apply commands like so: apply from: 'gradle/build-css.gradle' apply from: 'gradle/build-js.gradle' apply from: 'gradle/build-webserver.gradle' The buildscript‘s repository and dependency sections that define where to get the 3rd party CSS and JS plugins from can be moved into the relevant build-css and build-js.gradle files. However it’s not quite perfect. I did encounter a classpath issue with one of the JS plugins’ task and simply redefined the dependency in both the main projects build.gradle and the JS file. Overall though, keeping the concerns separate is much nicer for maintainability. Tests We used Jasmine with the JSTestDriver Jasmine plugin for this project. There was a Gradle Jasmine plugin but it’s no longer maintained and doesn’t work with the later versions of Gradle. That said, IntelliJ came to the rescue here and has a built-in Jasmine runner that automatically listens for source and test changes and reruns tests. For both our CI server and for kicking off tests from the command line, the gradle-js-plugin author wrote a task to kick off JSTD with Firefox in a headless browser. We adapted this for our build as follows: task jstd(type: Exec, description: 'runs JS tests through JsTestDriver') { commandLine = ['java', "-jar", "${configurations.testRuntime.files.find()}", '--config', "${projectDir}/jsTestDriverWithJasmine.jstd", '--port','9876', '--tests', 'all', '--testOutput', buildDir, '--browser', 'firefox'] // Then add --verbose flag to commandLine based on // the gradle log level. See later. } There are other JS testing frameworks supported. If you use buster, the Gradle plugin for buster.js keeps a warm VM running in the background checking for changes and re-running your tests. There is a grunt plugin, so you could just run whatever JavaScript tools you want. But as you learnt above, Grunt isn’t the new hotness anymore, so it may have limited appeal. ;-) For us, Grunt still relied on NPM so we left it alone. Quick start and CI friendly The other neat thing that Gradle has is a wrapper script that pulls down the version of Gradle you used to write your Gradle build script with. This is not only good for developers but for any build agents in your CI environment. You don’t need to have Gradle installed, you just clone the project and do gradlew jettyRun and it will go find Gradle, download and install it if it’s not there and then run gradle jettyRun as if you had it right from the beginning. It’s Groovy and it’s groovy If you are already a Javascript dev then much of the Groovy GDK will be easy to relate to. Groovy is dynamically typed as well, but the optional typing will be a welcome facility if you want to take it up. The tooling for Gradle and Groovy out of IntelliJ Community Edition (free) is quite neat. Netbeans (free) has recently made big strides in HTML5 and Javascript support and has strong Groovy support too. This may be a good opportunity to pick up another tool as well that will make your Javascript development easier too. Write your own tasks to do things in the build It’s easy to write your own Gradle tasks. We wrote our own task to modify the base HTML page of our single page webapp to load in different entry point scripts for development and production. We could then wire this task into the build, declare it as a dependency for producing the HTML for our war file or production like jetty deployment that referenced our combined minified gzipped CSS and JS files, or as a dependency to add in the head.js script that called all the individual dev scripts and css. enum DevMode { DEV, PROD, ORIGINAL }; task addProdBootLibs(type: WebAppBootLibTask, description: 'Modify the html of a single page app to include the dev or prod scripts and css') { html = file(webAppDirName + '/index.html') devMode = DevMode.PROD } task addDevBootLibs(type: WebAppBootLibTask, description: 'Modify the html of a single page app to include the dev or prod scripts and css') { html = file(webAppDirName + '/index.html') devMode = DevMode.DEV } task revertBootLibs(type: WebAppBootLibTask, description: 'Revert html to use a placeholder message to remind devs to run the correct script') { html = file(webAppDirName + '/index.html') devMode = DevMode.ORIGINAL } class WebAppBootLibTask extends DefaultTask { final def tag = "" final def endTag = "" @InputFile File html @Input DevMode devMode @OutputFile @Optional File outputHtml def devModeToInclude = [(DevMode.ORIGINAL): ORIGINAL_INCLUDE, (DevMode.DEV): DEV_INCLUDE, (DevMode.PROD): PROD_INCLUDE] final static def ORIGINAL_INCLUDE = """ Libraries Required Run the addDev/ProdBootLibs gradle task to add the correct libraries for the target environment to the project. """ final static def DEV_INCLUDE = """ """ final static def PROD_INCLUDE = """ """ @TaskAction def void applyScriptAndStylesheetTags() { if (html.exists()) { def htmlReplacement = html.getText().replaceAll( "$tag\\p{all}*$endTag", "$tag${devModeToInclude[devMode]}$endTag") logger.info(htmlReplacement) if (outputHtml == null) outputHtml = html outputHtml.setText(htmlReplacement, 'UTF-8') } } } Open your webapp after deploy Because it was Java, we could use the JDK’s java.awt.Desktop to open links to the webapp after a deploy was kicked off locally. import static java.awt.Desktop.* deployLocalMinified << { // open the webapp in your browser desktop.browse "http://localhost/report.html".toURI() } SCP, SSH Because Gradle comes installed with its own ant, it can run any Ant task. We used the ant SSH plugin to SCP our packed distributions to our build box and another task to execute an SSH script remotely to deploy. The deep dark Gradle cookie jar It can get easy to spoil yourself with all of Gradle’s features. The simple stuff is simple but it did take me a few reads of the manual to get my head around it. You can pick out the first 10 chapters and get the basics which should be enough to keep you out of trouble for a while. However venture a little bit out of the norm and then you have to start really thinking about reading a few more chapters. This isn’t a bad thing. The tool in itself is quite broad and the Gradleware team have done a good job of breaking the chapters up to focus on just functionality that you need. But you do get to a point where you start realising that you need to understand more of the Gradle build lifecycle, how external properties work, the different lifecycles of tasks. And I won’t sugarcoat it, since JavaScript is not an official plugin from Gradleware (yet) and not maintained full-time it still has a few quirks both in documentation and the occasional bug. The community around it is good, but you just have to be prepared to roll your sleeves up. The Open Source culture is alive and well both in the Gradle and Groovy communities. When getting stuck it’s pretty easy to clone a plugins git repo to understand how a plugin works. The issues page for the CSS/JS plugins do list a few workarounds too so it’s worth looking there. However, I appreciate that this is not for everyone, especially if you are a Javascript dev with no Java/Groovy experience. Learning this used up more time than I initially planned for Gradle but I do hope JavaScript devs can benefit from this blog to take it up. Also, the experience has been great to learn more about Gradle anyway and thus helped make me aware of things we can apply in our Java builds too. Other lessons learnt So I listed a whole bunch of positives earlier, here were some of the things that irked myself and my colleagues as we picked up the tool. Tasks. When to use << and the task keyword prefix. Some plugins add default tasks. In order to configure them you just go lesscss { source = css.dev.css.files // css sourceset dest = ”$buildDir/out.css” } But the properties are executed every time the script is run for any task. So say I wanted to log what my sourceset had evaluated to, my instinct would be to stick a logging message lesscss { source = css.dev.css.files// css sourceset dest=”$buildDir/out.css” logger.quiet(“Compiling these files $source into $dest”) } If you go run another task, you’ll see that this closure gets evaluated regardless of whether you asked it to or not. gradle –q combineCss Compiling these files [‘src/main/javascript/styles/foo.css’,’src/main/javascript/styles/bar.css’] into /home/username/projects/gradle-js-blog/build … :combineCss task executed successfully. ‘Compiling these files…’? What, thats from lesscss???? Why am I seeing another build task output? What you need is to define the task with doFirst or doLast (shorthand <<) in order to tell Gradle that block isn’t to run during build evaluation. It turns out to be is that lesscss isn’t a task at all. This is called a convention mapping and we are configuring properties in the build evaluation step of Gradle rather than steps to run as part of running the task. It’s a bit confusing since this is also a Groovy bit of code. You can do manipulation on those properties at build evaluation time which is neat but the difference isn’t immediately apparent. Getting the log level flag passed into Gradle Gradle comes with three logging flags. --quiet, --info and --debug. By default Gradle will only show if a task passed, failed or was skipped. Much unlike Maven. Thus, you’ll only use info or debug if you want to see the output of the steps. It would be nice for JS tools to mimic the log level that was passed into the Gradle build. The project object that’s available in every buildscript exposes its logger however calling logger.level returned null. A workaround described on the Gradle forums indicated that using the logger.isXXXXXXEnabled methods worked, so we wrote a task to query this and apply the correct loglevel to the task. task someTaskIdLikeToReuseTheLogLevelPassedToGradle(type: Exec) { // setup commandLine array with path to tool and other flags String runnerMode = getRunnerMode() if (runnerMode) commandLine += ['--verbose', '--runnerMode', runnerMode ] } private String getRunnerMode() { def runnerMode = null if (logger.isDebugEnabled()) { runnerMode = 'DEBUG' } else if (logger.isInfoEnabled()) { runnerMode = 'INFO' } else if (logger.isQuietEnabled()) { runnerMode = 'QUIET' } runnerMode } More to come from the official gradle guys There is more JS support coming. There is a large thread on the public gradle mailing list discussing putting more JS in. Already there are undocumented plugins for Rhino and Coffeescript (you can find these in the Javadoc for Gradle but missing from the users guide and DSL which indicate that they are in progress). File watching is also on the road map so hopefully more plugins can have the watch/build/reload cycle that the buster.js and grunt plugins afford. Haven’t yet covered Artefact resolution from a public repository like Github or Maven Central for JS libs is still a popular topic on the forums. The gradle-js-plugin covers the former with experimental support, and the Gradleware team are working on the later. Because of the problems I had getting npm to work over our proxy I stayed away from this anyway. However require.js support in the gradle-js-plugin out of the box which will get you some of the way there. Build Successful Projects with JavaScript only web front ends are ubiquitous nowadays. Being able to use the same tools to build your front and back end infrastructure allows your team to have a better sense of the project as a whole. There are a number of things that make Gradle attractive – an out of the box web platform to develop with, smart tasks that are more aware of their dependencies, the ability to integrate with the JDK and existing Java tools and the ability to fall back to calling JS command line tools – all provide a huge amount of flexibility. As a Java and Groovy dev, using a familiar tool in Gradle to build for a completely different platform was both a learning and rewarding experience. It did come with its frustrations particularly as you step from pre-configured conventional Gradle build tasks to more specific ones but I think we became better build masters for it. Although I haven’t touched on it in this blog, being able to integrate with a Java web container also availed us to some quick wins when it came to implementing security for our single page webapp. We could rely on the Java Servlet 3 spec to make light work of the authentication and authorisation aspects of our app. In addition, we can package our JavaScript app up as a war and give it to our clients deployment team who just think they are deploying a Java app. Since that’s the infrastructure they maintain and are used to then that works out well for everyone. There is no stopping the JavaScript build tool train, at least for a while. For JavaScript devs looking at their next build tool replacement, Gradle is a worthy choice for any platform. Sure you have to learn Groovy, but it’s not a bad language and JS devs shouldn’t have much trouble adjusting. You may find that if you try Gradle, Groovy and a Java based IDEs and then decide it’s not for you, at least you’ve got some exposure to new tools you didn’t expect to have in your tool box as well as an awareness of how other JS build tools could behave. And that, in my mind at least, is a nice prize for any software practitioner to come away with. This blog was written with Gradle 1.10
March 24, 2014
by Kon Soulianidis
·
38,897 Views
·
4 Likes