Tag Archives: build script

MSBuild: CSS compression and Javascript minification for ASP.NET MVC

At work we use a continuous integration system to monitor our source repository and create builds.  It’s very handy in that us developers can simply code all day and then commit at the end of the day and then magically what we’ve written gets compiled, configured and deployed to our test and integration environment.  The deployment process is a simple file copy, but what’s smart is that only your standard aspx pages and dll’s get copied over – all those source files don’t get copied and web.config can also be changed on the fly to allow for different environments (e.g. different database connection strings).

So being the lazy coder that I am I thought it’d be nice if I tried to replicate this to some extent at home for my personal projects.  It’s also very convenient since I set up and wrote the build scripts we use at work – at least I won’t be fumbling too much in the dark this time round 😉

I have no intention of setting up a continuous integration server at home (we use CruiseControl.NET at the office), so a simple batch file should do the trick.  What I wanted with a simple double click was:

  1. In place compilation of all my projects within a solution
  2. Copy only what’s required  to my “deployment” folder (i.e. no source files, only aspx, ascx, asax, js, css etc)
  3. Minify and compress my javascript and css
  4. Change web.config so it knows which database to connect to (production or dev) – future requirement
  5. Automate running unit tests – future requirement

Reasons for wanting the above:

  1. Since I’m doing all this on my home/development PC there’s really no need for step 1.  When I run the website locally the files should be compiled automatically, however, during the automated build process will be set to build in “Release” mode.
  2. I don’t like my source code hanging around on a production server.  It has no business being there even if IIS won’t serve it.
  3. Minification and compression make for happy end users
  4. Remember the last time you deployed a config file to production with the connection string to your local database?  Yeah I haven’t done that in a while 😉
  5. I use ReSharper to run my unit tests, but rarely (if ever) do I run the whole suite of tests at once.  At work I’ve set the build script to do this, but this will be just a simple script for me at home.  In future I will probably include this though.

Pre-Requisites

To achieve the above you will need to download and install the following:

[Note that apart from YUI the other two are optional but allow me to do stuff easier.  The Community Tasks probably isn’t even needed in this simple example, but I’m sure I used it for the build script at work which I’m liberally using as my base example)

Put those DLL’s in a place you’ll remember – we’ll need them later.

To achieve this automated build process we’ll need 2 files.  One will be a batch file calling MSBuild with various options and the other will be the proj file which contains all the build instructions.

BuildForProduction.cmd

@echo off
echo Creating a Production Build
path=%path%;C:WindowsMicrosoft.NETFramework64v3.5
MSBuild "C:UsersRezaDocumentsVisual Studio 2008ProjectsBillManagerBuildScript.proj" /v:n /m /tv:3.5 /p:TargetFrameworkVersion=v3.5;Configuration=Release;Platform=AnyCPU /t:BuildCode /fl

A very simple batch file.  First thing is to set the path so that we can find the MSBuild executable.  Note that I’m using 64 bit Windows 7 Professional, so if you’re running a different OS just remove 64 from the path.

Next is the command that does all the good stuff.  We call MSBuild with the options that we want to execute.  I won’t go into details, you can MSBuild /? to look at all the available options.  All I’m telling MSBuild with that line is that I want it to log with normal verbosity to the screen, use as many processors as it can find (all 8 baby!), use version 3.5 of the .NET framework and build it in “Release” mode.  Right at the end I also tell it which Target I want it to run (BuildCode).

The build script itself is rather large so I’ll try and split it up to try and make better sense of it. The thing to remember is that this script is essentially just a project file – the same as when you create a new project in Visual Studio (csproj or vbproj), so unfortunately we’re dealing with XML here (yuck).  I’ll just go over highlights here – I’ll include both files at the end of this post.

BuildScript.proj

At the very start of the file we have the following line:

<Project DefaultTargets="BuildCode" InitialTargets="GetPaths;GetProjects" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

What this basically says is that if no Target is specified, then use the BuildCode target.  However, the InitialTargets command states that those two Targets need to be run before anything else.  As can be deduced from the target names what I do there is get my pathing in order and create a collection of projects that I want to compile.

<Target Name="BuildCode">    
    <!-- Build the assemblies -->
    <MSBuild Projects="@(CodeProjects)"
             Targets="$(BuildTargets)"
             BuildInParallel="true"
             Properties="Environment=Configuration=$(Configuration);Platform=$(Platform)">
      <Output TaskParameter="TargetOutputs"
              ItemName="CodeAssemblies"/>     
    </MSBuild>

    <!-- Add the compiled code assemblies to master list of all compiled assemblies for the build -->
    <ItemGroup>
      <CompiledAssemblies Include="@(CodeAssemblies)" />
    </ItemGroup>

    <Message Text="Completed BuildCode task"
            Importance="high"/>

    <CallTarget Targets="CopyToBuildFolder" />
    
  </Target>

Above is the Target that we invoked from the batch file. What it’s effectively stating is that I want to build all the projects as defined in the collection @(CodeProjects).  There’s more to it but for this example we’re not using any of the other parameters.  The last command is where we tell the BuildCode target to call another target called CopyToBuildFolder

<Target Name="CopyToBuildFolder">
    <!-- Copies all compiled code to the correct folder - ready for deployment -->
    
    <!-- We need to delete all files in this folder first to ensure a clean build-->
    <Folder.CleanFolder Path="$(BuildsFolder)" Force="True" />

    <!-- Copy main website files - This is ASP.NET MVC specific -->
    <CreateItem Include="$(ProductName).Web**Views***.aspx;
                         $(ProductName).Web**Views***.ascx;
                         $(ProductName).Web**Views***.config;                        
                         $(ProductName).Web*.config;             
                         $(ProductName).Web*.asax;
                         $(ProductName).Webdefault.aspx;
                         $(ProductName).Web**bin***.dll;
                         $(ProductName).Web**content***.css;
                         $(ProductName).Web**content***.jpg;
                         $(ProductName).Web**content***.gif;
                         $(ProductName).Web**content***.png;
                         $(ProductName).Web**scripts960.gridder.js;
                         $(ProductName).Web**scriptsbillmanager.js;
                         $(ProductName).Web**scriptsjquery.tablesorter.min.js;
                         $(ProductName).Web**scriptsjquery-1.3.2.min.js;
                         $(ProductName).Web**scriptsjquery-ui-1.7.2.custom.min.js;
                         $(ProductName).Web**scriptsjson2.js;">
      <Output TaskParameter="Include" ItemName="FilesForWeb" />
    </CreateItem>

    <!-- copy the files to the production build area-->
    <Copy SourceFiles="@(FilesForWeb)"
          DestinationFolder="$(BuildsFolder)%(RecursiveDir)" />

    <!-- Change the Configs -->
    <CallTarget Targets="UpdateProductionConfig" />
    
    <!-- Compress any JS and CSS -->
    <CallTarget Targets="CompressAndMinifyJavascriptAndCSS" />
    
  </Target>

In this Target I’m specifying all the files that I want to include for deployment.  The first thing to do is ensure that the destination folder is clean, so I run a task that deletes all files in a folder.  This is where the SDC tasks come in handy.  The command to recursively delete files in a folder with standard MSBuild tasks is a pain, so by including that one DLL I have saved a lot of time.  The next step is to specify which files I want to copy across.  My solution contains one ASP.NET MVC project and 3 class library projects, so all I want to deploy is what’s in the Web folder.  Note the slightly odd syntax with the “**” in the path.  What this tells MSBuild is that I want to select all files recursively.  So the first line where in the CreateItem task is to get all aspx files in the Views folder recursively.  If you’re familiar at all with ASP.NET MVC and its folder structure then you’ll know why I need all these files.

So once I’ve created the collection of files I want (called @(FilesForWeb)) I call the Copy task and specify the %(RecursiveDir) command to ensure that the folder hierarchy is maintained.  Once that’s done I then call the next two Targets.

<Target Name="CompressAndMinifyJavascriptAndCSS">
    <!-- Compresses javascript and CSS. combines into one file -->
    <CreateItem Include="$(BuildsFolder)ContentSite.css">
      <Output TaskParameter="Include" ItemName="CSSFiles"/>
    </CreateItem>

    <CreateItem Include="$(BuildsFolder)scriptsbillmanager.js;">
      <Output TaskParameter="Include" ItemName="JSFiles"/>
    </CreateItem>
    
    <Attrib Files="%(JSFiles.FullPath)" ReadOnly="false" />

    <CompressorTask
        CssFiles="@(CSSFiles)"
        DeleteCssFiles ="false"
        CssOutputFile="$(BuildsFolder)Contentsite.css"
        CssCompressionType="YuiStockCompression"
        JavaScriptFiles="@(JSFiles)"
        ObfuscateJavaScript="yes"
        PreserveAllSemicolons="false"
        DisableOptimizations="false"
        EncodingType="UTF8"
        DeleteJavaScriptFiles="false"
        LineBreakPosition="-1"
        JavaScriptOutputFile="$(BuildsFolder)scriptsbillmanager.js"
        LoggingType="ALittleBit"
        ThreadCulture="en-GB"></CompressorTask>

    <CallTarget Targets="CompressJson2" />
  </Target>

We’re almost done here… The purpose of this last Target is to compress and minify my application specific javascript and css.  What we can do here is also combine all the javascript into a single file to be more efficient, but for now I’m only compressing single files (jquery and gridder are already minified). I think the Target is fairly self explanatory.  I’m compressing my application specific CSS and javascript and then saving it to itself.  What minification does is try to compact the javascript file as much as possible.  It’ll run through your script and replace long winded variable names with much shorter ones and try not to break your logic.  It also removes any comments, so you can be as verbose as you like whilst developing but when it gets pushed to a live website those funny remarksdeep insights into your code are hidden from your public audience.  Note that I’m also setting the encoding to UTF8.  This is useful if you need to do localisation within your javascript.

Phew… long post.  Scripts for this can be found here.  Note that this is a sample only – use it at your own risk!