Ad-Hockery

ad-hockery: /ad·hok'@r·ee/, n.
Gratuitous assumptions... which lead to the appearance of semi-intelligent behavior but are in fact entirely arbitrary. Jargon File

Using SASS and Compass with Gradle

I recently started helping with the Ratpack website. It is (or will be) a Ratpack app & built with Gradle. I started prototyping with a simple webapp created with Yeoman and using SASS and Compass for authoring CSS. When I migrated the work-in-progress into the ratpack-site application I initially used Ted Naleid’s method of calling Yeoman’s Grunt tasks from Gradle. Unfortunately this meant there were rather a lot of build dependencies. In order to build the app you would need Node.js, Ruby and the Compass gem installed. Peter Ledbrook pointed out this could frustrate potential contributors & Marcin Erdmann proved an example of what he meant. Clearly I needed to simplify.

SASS, particularly with Compass, is great for CSS authoring. Mixins & functions like contrasted or image-height and the vertical rhythm support have become invaluable to me. However, they are Ruby gems with no ports to other languages.

I wanted to at least try to get it working before giving up and using LESS. I found a few posts where people had done something similar but mostly they were either half-complete, using older Gradle syntax or exhibiting the same dependency problems I already had. They did give me the idea to use JRuby to run SASS, though.

I came across several solutions that advised either packaging desired gems in a jar file or actually re-packing the JRuby jar with additional gems installed. Neither of these appealed to me. I didn’t want to publish new binary artifacts or untangle any potential licensing issues. It seemed to me that I should be able to use JRuby to install the gems I needed somewhere local to the build and then refer to them.

Installing Ruby gems

First I created a Gradle task called installGems to install the compass gem. This uses the Gradle JavaExec task to run gem install using JRuby and specifies the path to install the gems as .jruby/gems. That directory is also the task’s output so that further tasks can depend on it as part of the incremental build.

JRuby itself is added to a new dependency scope since I’m only using it for running build tasks, not as part of the application itself.

configurations {
    jruby
}

dependencies {
    jruby 'org.jruby:jruby-complete:1.7.3'
}

ext {
    gemsDir = file('.jruby/gems')
}

task installGems(type: JavaExec) {
    outputs.dir gemsDir

    classpath = configurations.jruby
    main = "org.jruby.Main"
    args "-S gem install -i $gemsDir --no-rdoc --no-ri compass".tokenize()

    doFirst {
        gemsDir.mkdirs()
    }
}

Initially I installed the gems under build but that would mean any time gradle clean is run they will be deleted. Since they are not particularly fast to install and shouldn’t change I moved them elsewhere.

Compiling SASS

Next I created a compileSass task. The trick is to supply two environment variables; GEM_PATH which tells JRuby where to look for installed gems and PATH pointing to the .jruby/gems/bin directory where the compass executable is installed.

The compileSass task’s inputs are the outputs of installGems – meaning installGems is run automatically if necessary – along with the directories containing my .scss files, images and JavaScript. The task’s output is the directory with the compiled CSS.

ext {
    cssDir = file(/* output dir where CSS should go */)
    sassDir = file(/* location of .sass / .scss files */)
    imagesDir = file(/* location of images */)
    javascriptsDir = file(/* location of javascripts */)
}

task compileSass(type: JavaExec) {
    outputs.dir cssDir
    inputs.files installGems
    inputs.dir sassDir
    inputs.dir imagesDir
    inputs.dir javascriptsDir

    classpath = configurations.jruby
    main = "org.jruby.Main"
    args "-S compass compile --sass-dir $sassDir --css-dir $cssDir --images-dir $imagesDir --javascripts-dir $javascriptsDir --relative-assets"
    environment 'GEM_PATH', gemsDir
    environment 'PATH', "$gemsDir/bin"

    doFirst {
        cssDir.mkdirs()
    }
}

I also added compileSass into the task dependency chain so it would automatically run when needed:

processResources.inputs.files compileSass
clean.dependsOn cleanCompileSass

I then created my own JRubyExec task type that extends JavaExec to bundle together some of the commonalities between installGems and compileSass.

Watching for changes

The next step was to use the compass watch command to automatically recompile SASS changes while the application is running. The ratpack-site app uses the Gradle application plugin to provide a run task. I needed to create a background thread that runs compass watch. I couldn’t figure out how to re-use my JRubyExec task here as I needed to use the imperative style of calling project.javaexec so I’ve ended up with some duplication. It does work, though, which is the main thing.

task watchSass {
    doFirst {
        cssDir.mkdirs()

        Thread.start {
            project.javaexec {
                classpath = configurations.jruby
                main = 'org.jruby.Main'
                args "-X-C -S compass watch --sass-dir $sassDir --css-dir $cssDir --images-dir $imagesDir --javascripts-dir $javascriptsDir --relative-assets".tokenize()
                environment 'GEM_PATH', gemsDir
                environment 'PATH', "$gemsDir/bin"
            }
        }
    }
}
run.dependsOn watchSass

Performance tuning

Once everything was working I spent a little time performance tuning. The JRuby wiki has some tips about JVM flags useful for JRuby execution & I added -client -XX:+TieredCompilation -XX:TieredStopAtLevel=1 which improved things a little. Following a tip found on Charles Nutter’s blog I also added -X-C to the JRuby command itself which disables the JRuby JIT compiler which also seemed to help a little.

Compiling SASS this way is still slower than using native Ruby – either standalone or via Grunt – but it’s not painfully slow and the tradeoff in terms of build simplicity is worth it.

Next steps

This is not a perfect or finished solution. It contains some duplication, a mixture of declarative and imperative task styles, no proper sourceSet for SASS. When time permits I’d like to get this bundled up as a proper Gradle plugin or possibly two – one for generic JRuby execution and another specifically for SASS.

I should also point out that Marcin Erdmann and Luke Daley were a big help in getting this soluton working. My Gradle-fu is shaky at best and they helped me a lot with declaring the incremental build properly and getting the background thread for the watchSass task working.

Web Statistics