Tuesday, February 1, 2011

Using Sass in an Ant build

I recently started using Sass in a side project I've been working on.  It allows me to do two neat things:

  1. Declare my site's colors as variables and re-use them everywhere.
  2. Use only meaningful classes in my markup and style those classes appropriately without copying and pasting a lot of CSS.  For example, if I want an invoice to be displayed in a groupbox, I can put the invoice data inside of a div and assign "invoice" as its class.  Then I can use the Sass mixin feature to say that the invoice class should be styled like a groupbox, whose style I define only once.

However, Sass is a Ruby tool and I'm working in Java.  The project uses Ant for its builds and I wanted a way of compiling my Sass code into CSS in an automated way.  What I ended up doing was using Ant's <apply> task to find all the .sass and .scss files in my project and run them through the Sass processor.  This is what the task ended up looking like:

<target name="sass-compile" depends="properties">
    <apply executable="sass" dest="${project.src.dir}" verbose="true" force="true" failonerror="true">
        <srcfile />
        <targetfile />

        <fileset dir="${project.src.dir}" includes="**/*.scss,**/*.sass" excludes="**/_*" />
        <firstmatchmapper>
            <globmapper from="*.sass" to="*.css" />
            <globmapper from="*.scss" to="*.css" />
        </firstmatchmapper>
    </apply>
</target>

Everything is pretty straight forward with one possible exception.  By convention, partials (shared files that are only intended to be imported by other Sass files) begin with an underscore.  I excluded them from the fileset since they aren't intended to be used directly and my build automatically rolls CSS files into the .war file.

But there's one more import-related caveat.  When used with a mapper, the <apply> task only runs the executable on input files that have been modified more recently than the corresponding destination file.  That's a problem because a Sass file might not have changed but one of its imports could have, in which case it should be re-compiled.  To make sure the build happens every time, I used force="true".