Website builds using Make

Published 19:17 on 26 February, 2010

In the interests of improving quality in production, of eliminating repetitive tasks, and of general development time saving, it’s often a good idea to automate some of the website build process. What do I mean by “website build process”? Put simply, the task of preparation and publication to production (your live, open-to-the-internet environment), from a development environment.

In many cases, this may only be a push from your local machine to your web server, via SCP (or worse, FTP). However, if you’re working with any level of intricacy—or if you’re working in a team environment—you will, no doubt, have experienced (amongst other things) the logistical nightmare of multiple CSS and JavaScript files: First building page specific style sheets, so you maintain a single HTTP request for each, and secondly compressing those files so that any unwanted elements are removed for production (e.g. comments, needless whitespace etc.). If you’re not sure why you’d want to do these things, I’d recommend having a look at Steve Souder’s “High Performance Web Sites: Essential Knowledge for Front-End Engineers”.

In this post, I’m going to look at how you can automate the CSS and JavaScript part of the build process using Make, a handy little program that is installed with the standard build tools on most *nix based systems.

What we want to achieve

In the future, we may wish the build to pull completed code from a source-control repository (i.e. Git or SVN) down to a staging environment, and the subsequent push from staging to production. We may also wish our build to handle the pushing of static assets—including images—to a CDN, like Amazon Cloudfront, or its platform, Amazon S3.

For the purposes of this article, we simply want to handle the generation of page-specific CSS and JavaScript static assets. This is by no means a complete build, but it’s as good a place to start as any. So let’s break down that process into steps, to better understand it:

  1. We want to amalgamate our various style sheets, or JavaScripts, into a single, page-specific sheet, so that we can reduce HTTP requests for the page. Imagine we've developed our pages to include CSS like so:
    /index.html
    <link rel="stylesheet" href="reset.css" type="text/css">
    <link rel="stylesheet" href="core.css" type="text/css">
    <link rel="stylesheet" href="top-articles.css" type="text/css">
    <link rel="stylesheet" href="tna-module.css" type="text/css">
    
    /article.html
    <link rel="stylesheet" href="reset.css" type="text/css">
    <link rel="stylesheet" href="core.css" type="text/css">
    <link rel="stylesheet" href="article-content.css" type="text/css">
    <link rel="stylesheet" href="tna-module.css" type="text/css">
    As you can see, both pages include several of the same style sheets. However, they also each have different sheets, which means we can't really have One Big Style Sheet To Rule Them All™ (a "method" I've had to suffer so often in the past on large corporate projects) without including every style ever used across the entire site. That would mean downloading styles that could be entirely redundant in the current page. If we adopt a build process to combine style sheets for a page based on what is actually needed, we can generate page-specific sheets without having to maintain duplicated style rules (which would be the case if we maintained page-specific style sheets manually). This means we reduce our HTTP requests for CSS to a single link per page:
    /index.html
    <link rel="stylesheet" href="page-index.css" type="text/css">
    
    /article.html
    <link rel="stylesheet" href="page-article.css" type="text/css">
    This also rings true for JavaScript, which should be treated in exactly the same way.
  2. We'd like to compress our page-specific style sheets and external JavaScript using minification. In my case, I tend to use the YUI Compressor tool, since it handles both JavaScript and CSS. There's really no reason why you couldn't use separate tools for each; Google Closure Compiler, for example.
  3. The new page-specific files will require versioning so that the filenames change each time they are updated. This means we can adopt a far (or, at least, moderately far) future Expires HTTP header to better make use of caching. You can read more on this in the YDN performance best practices under "Add an Expires or a Cache-Control Header". There are several methods I could adopt for this:
    • Add a version number on the end of the filename:
      styles.1.0.css
      This means a certain degree of management in terms of major and minor releases, and often results in amusing version numbers like 1.132.
    • Add some form of timestamp to the filename:
      styles.20100221170134.css
      Generation of timestamps may not be consistent across servers, particularly if you're working in a load balanced environment (i.e. the files are duplicated across front-end servers). Also, the filenames, and any related strings, are much longer.
    • Create a hash (MD5, SHA etc. of the updated file and use that, or a trimmed version of it, as the filename:
      6c7f5ed2.css
      This is a method I've only recently been introduced to, but one that clearly has its advantages. Firstly, hashing the file means we will have exactly the same result on all servers, since the file itself will be the same. It also means we don't need to remember previous version numbers. Finally, there's really no reason why we can't trim the filename down to 8 characters, since the chances of generating duplicates would still be significantly low—and if we do get a duplicate, we can regenerate easily anyway.
    You've probably figured out that I intend to use the latter hashing method in my build, since it is probably the most robust solution and the one that I prefer.
  4. The HTML that links to our external static files will need to be updated to reflect the new versions of our built files. This simply means finding and replacing the old values.

We want to achieve these four steps with the minimum possible fuss, so we should be aiming for a “single button build process” where we can literally click a button, or run a single command to complete the build. Just because we’re only writing a small part of the final process here, doesn’t mean we should approach our solution any differently; after all, the final process will probably only run specific sub-builds in turn.

Choosing a build method

Scripting a build process on the server can be achieved in several ways:

  • Shell scripting: available in all operating systems and a good way to automate common command line tasks.
  • Server-side scripting languages: Perl, Python, Ruby, or even PHP. Any script that can be run from the command-line and that has access to the file-system would be fine. Since we're running a web server, most of the above are probably already installed.
  • Specific build languages: Make, Rake, Ant, NAnt, or MSBuild. There are plenty of these around, each with its own pros and cons. They are specifically designed for the build process and, as such, are a perfect fit for what we want to achieve.

As previously stated, I’m using Make, which is pre-installed on almost every *nix-based system (it wasn’t pre-installed on my JeOS VMs, but was trivial to install from packages—and is part of the “build-essential” package you may have already installed).

Using Make

Make basically processes a configuration known as a “Makefile”. The Makefile contains a list of all target files to be created, and the commands to create them.

A simple Makefile consists of “rules” with the following structure:

target … : prerequisites …
	command
	…
	…
	…

Where:

  • target is usually the name of a file that is generated by a program or a set of commands; in our case, this is going to be the amalgamated and minified page-specific static files. A target can also be the name of an action to carry out, such as "clean".
  • prerequisite is a file (or another target) that is used as input to create the target. A target is often likely to depend on several of these.
  • command is an action that Make carries out. A rule may have more than one command, each on its own line.

If you’re interested in learning more specifics about Make, I’d recommend taking a look at the GNU Make manual. The manual is available in a bunch of different formats.

Building static assets

So now that we understand the (very) basics of Make, let’s try writing a Makefile for our proposed build. I’m going to gradually build up a Makefile in this tutorial, so that it’s easy to cut and paste at any given point.

To begin, create a Makefile in your static assets directory like so:

$ vim /path/to/staticfiles/Makefile

It’s best to use a capital M on the Makefile simply to make sure it appears at the top of any ll -l commands. You may also see all caps used. Personally, I prefer the single capital M.

Let’s start by adding a comment header, defining a target for our CSS, and also the prerequisites required:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

/path/to/staticfiles/css/page.css: /path/to/staticfiles/css/src/reset.css /path/to/staticfiles/css/src/core.css /path/to/staticfiles/css/src/top-articles.css /path/to/staticfiles/css/src/tna-module.css

Straight away you should see a problem; there’s a lot of repetition of paths there. We can probably adjust that a little by assuming the Makefile is in the /path/to/staticfiles/ directory, and replacing that with ./. However, we could do a better job if we use a Makefile variable like so:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path = /path/to/staticfiles/css/

$(css-path)build/page.css: $(css-path)src/reset.css $(css-path)src/core.css $(css-path)src/top-articles.css $(css-path)src/tna-module.css

Here you can see we’ve defined the absolute path as a variable at the top of the Makefile, and are then reusing it in both our target and our prerequisites—using an absolute path seems safer to me, but you may disagree. If we plan ahead a bit more, we can add a few more variables like so:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

$(css-page-target): $(css-page-prereq)

Here I’ve added the prerequisites in a space-separated list, since I can use them in that format for the prerequisites, and again when I make use of the cat command later…

Concatenating the files

We now have a target for Make to examine, and a list or prerequisites that our Make rule will depend upon before it runs.

The first set of commands should handle the first stage in our build; the concatenation of the files into a single file:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"

Ok, so I have a few syntactical things to explain here:

  • When adding a command to a rule, make sure the command line starts with a tab character. This is required, and if you're using an editor that uses the mythical "soft tab" (i.e. spaces) you'll enter unto a world of woe.
  • If a line starts with '@', the echoing of that line is suppressed (but not any output from that command). This just means I'm hiding the actual commands in the process; if any errors occur, those will appear in stderr.
  • Ending an echoed string with a \c character prevents echo printing the default trailing newline character. I'm using this to allow me to display "[ Done ]" on the same line, assuming no problems have occurred.

Right, now that I’ve explained a little of the syntax, let’s take a look at what those commands actually do:

First of all, we make sure the target file doesn’t exist by forcing an rm. Then I’m echoing a message to show the CSS build has started. Next I cat the requisites together into a temporary file. Once this is all complete, I echo a “done” message (which will appear on the same line as the start message, since I ended the first echo with a /c character).

Minification

Now we have file containing all the prerequisite CSS files together, we want to minify it with the YUI compressor:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\t\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"

Here I’ve specified the path to the YUI Compressor .jar file as another variable and have run our temporary CSS file through it, outputting to the target file. I’ve also wrapped that process in the same echo trick as before.

Next, we just need to clean up that temporary file, and this rule is complete:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

Now if we run Make, our merged and compressed CSS file will be built:

$ cd /path/to/staticfiles
$ make
Merging CSS files…		[ Done ]
Compressing merged CSS…		[ Done ]
$ ls -lah css/build
total 8
drwxr-xr-x  3 auser  agroup   102B 23 Feb 14:17 .
drwxr-xr-x  5 auser  agroup   170B 19 Feb 22:45 ..
-rw-r--r--  1 auser  agroup   2.9K 23 Feb 14:17 page.css

Great!

At this point, it’s worth sense-checking the contents of your new file, to see that all your prerequisite files have been included, and that it has been correctly minified. Assuming everything is correct, let’s duplicate that rule for JavaScript files:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

I’m using two separate rules here; one for CSS, and one for JavaScript. If I run Make at this point, only the first rule (for the CSS) will be run. This is because Make only ever runs the first rule automatically. If I want both rules to be run, I should set up another rule that defines these rules as prerequisites.

Cleaning up old builds

It’s a good idea to define a “clean” target that removes all the files that are built in the Makefile. This means the user can start-over easily if they so desire:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

all: $(css-page-target) $(js-page-target)

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

clean:
	@rm -f $(css-page-target)
	@rm -f $(js-page-target)

To run a specific target, rather than the first in the list, you simply pass it as an argument to Make. Let’s run our clean target like so:

$ make clean

Now if you run Make again, it should build both the CSS and the JavaScript page files:

$ cd /path/to/staticfiles
$ make
Merging CSS files…		[ Done ]
Compressing merged CSS…		[ Done ]
Merging JS files…		[ Done ]
Compressing merged JS…		[ Done ]
$ 

Hash pipe (cut)

The next step is to hash our new page-specific CSS and JS files, and copy them to a higher level (so we don’t need to have the “build” directory on production). To do that we’ll want to pass the contents of the file to something that will generate a SHA1 checksum. On some systems, you can use shasum, but on my Mac that didn’t exist, so I’m using openssl sha1 instead, like so:

$ cat file | /usr/bin/openssl sha1

This will output a full SHA1 checksum to stdout. We actually want to use it as our filename, and the full checksum is likely to be too long. We can fix that by using the cut command as follows:

$ cat file | /usr/bin/openssl sha1 | cut -c1-8

Now if we want to use that as a filename, we can generate a variable in our Makefile. Let’s do that for both the JavaScript and the CSS:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

all: $(css-page-target) $(js-page-target)

install: css-build = `cat $(css-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.css
install: js-build = `cat $(js-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.js
install: all

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

clean:
	@rm -f $(css-page-target)
	@rm -f $(js-page-target)

Here I’ve added a new “install” target, which uses the “all” target as its only prerequisite. I’ve also defined two variables above the target as target-specific variables. This scopes the variables to this rule alone.

I’ve used the target “install” to conform with accepted Make standards. This means that, each time we want to refresh the CSS, we only need to type the following (which is another accepted standard):

$ make clean install

As you can see, you are able to pass multiple targets into the make command as arguments.

Using the hash for good, not evil

This is all very well and good, but even though we have the new filename stored in a variable, we’re not doing anything with it. Let’s rectify that:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

all: $(css-page-target) $(js-page-target)

install: css-build = `cat $(css-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.css
install: js-build = `cat $(js-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.js
install: all
	@cp $(css-page-target) $(css-path)$(css-build)
	@cp $(js-page-target) $(js-path)$(js-build)

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

clean:
	@rm -f $(css-page-target)
	@rm -f $(js-page-target)
	@rm -f $(css-path)*.css
	@rm -f $(js-path)*.js

Here I’ve copied the files to the higher css-path/ and js-path/ directories. This means I won’t need to sync the build/ and src/ directories to production.

I’ve also added two rm commands to the “clean” target that remove all built CSS and JS files in the respective directories, regardless of name. Note: make sure you’re not doing this recursively, otherwise you’ll trash all your valuable source files.

Now if you look in the css-path/ directory, you should see something like:

$ ls -lah
total 4.0K
drwxr-xr-x 5 auser agroup  170 2010-02-25 22:36 .
drwxr-xr-x 7 auser agroup  238 2010-02-25 22:14 ..
-rw-r--r-- 1 auser agroup 2.9K 2010-02-25 22:36 6c7f5ed2.css
drwxr-xr-x 3 auser agroup  102 2010-02-25 22:36 build
drwxr-xr-x 4 auser agroup  136 2010-02-25 21:54 src
$

Great, so now we have a (reasonably) unique filename for our concatenated and minified CSS; and if you look in your JavaScript directory, you should see something similar.

The next step is to actually find and replace the inclusion of your file in your HTML.

Automagically updating the HTML

We basically need to run a find and replace on the HTML that includes your external CSS and JavaScript files, whether that is an SSI or a full HTML page. To do that, we should probably specify those files as variables, so that they can be easily maintained:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

css-inc-file =		/path/to/publicfiles/page.html
js-inc-file =		/path/to/publicfiles/page.html

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

all: $(css-page-target) $(js-page-target)

install: css-build = `cat $(css-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.css
install: js-build = `cat $(js-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.js
install: all
	@cp $(css-page-target) $(css-path)$(css-build)
	@cp $(js-page-target) $(js-path)$(js-build)

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

clean:
	@rm -f $(css-page-target)
	@rm -f $(js-page-target)
	@rm -f $(css-path)*.css
	@rm -f $(js-path)*.js

I’ve added two variables here in case, at some point in the future, we decide to break the inclusions out into separate files.

Now we’ve defined those files in the Makefile, we should really use them for something. I’m going to use sed to find and replace the references to the external static files:

#========================================
# Static Asset Build Makefile
#
# Author: Tim Huegdon
#========================================

domain =			yoursite.com

css-inc-file =		/path/to/publicfiles/page.html
js-inc-file =		/path/to/publicfiles/page.html

css-path =		/path/to/staticfiles/css/
css-page-target =	$(css-path)build/page.css
css-page-prereq =	$(css-path)src/reset.css \
			$(css-path)src/core.css \
			$(css-path)src/top-articles.css \
			$(css-path)src/tna-module.css

js-path =		/path/to/staticfiles/js/
js-page-target =	$(js-path)build/page.js
js-page-prereq =	$(js-path)src/carousel.js \
			$(js-path)src/awesome.js \
			$(js-path)src/beacon.js

yui-jar =		~/tools/yui/yuicompressor-2.4.2.jar

all: $(css-page-target) $(js-page-target)

install: css-build = `cat $(css-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.css
install: js-build = `cat $(js-page-target) | /usr/bin/openssl sha1 | cut -c1-8`.js
install: all
	@cp $(css-page-target) $(css-path)$(css-build)
	@cp $(js-page-target) $(js-path)$(js-build)
	@echo "Linking to updated CSS and JavaScript…\t\c"
	@sed -i '' "s/http:\/\/static\.$(subst .,\.,$(domain))\/css.[^\"]*/http:\/\/static\.$(subst .,\.,$(domain))\/css\/$(subst .,\.,$(css-build))/" $(css-inc-file)
	@sed -i '' "s/http:\/\/static\.$(subst .,\.,$(domain))\/js.[^\"]*/http:\/\/static\.$(subst .,\.,$(domain))\/js\/$(subst .,\.,$(js-build))/" $(js-inc-file)
	@echo "[ Done ]"
	@echo "Installation is complete."

$(css-page-target): $(css-page-prereq)
	@rm -f $(css-page-target)
	@echo "Merging CSS files…\t\t\t\c"
	@cat $(css-page-prereq) > $(css-path)build/tmp.css
	@echo "[ Done ]"
	@echo "Compressing merged CSS…\t\c"
	@java -jar $(yui-jar) -o $(css-page-target) $(css-path)build/tmp.css
	@echo "[ Done ]"
	@rm -f $(css-path)build/tmp.css

$(js-page-target): $(js-page-prereq)
	@rm -f $(js-page-target)
	@echo "Merging JS files…\t\t\t\c"
	@cat $(js-page-prereq) > $(js-path)build/tmp.js
	@echo "[ Done ]"
	@echo "Compressing merged JS…\t\c"
	@java -jar $(yui-jar) -o $(js-page-target) $(js-path)build/tmp.js
	@echo "[ Done ]"
	@rm -f $(js-path)build/tmp.js

clean:
	@rm -f $(css-page-target)
	@rm -f $(js-page-target)
	@rm -f $(css-path)*.css
	@rm -f $(js-path)*.js

The first thing I’ve done here is to define another variable for the domain name of the site. This is because I’m going to need to use it to identify the links to the filenames in the regular expression.

Next I added the sed commands to our “install” target, using the -i option (edit files in-place). The regular expression is basically looking for a reference to http://static.yourdomain.com/css/something and replacing it with the full URI to our newly generated file.

Once this step is finished, the installation of our newly built static files is complete; and, as such, so is our Makefile. As previously stated, to run a new build—that also cleans up old builds—simply type the following:

$ make clean install

A better development environment

Instigating a build process in your environment allows you the flexibility to design your development environment for ease of maintenance, rather than having to think about optimisation and structure for production. Your development environment should be quite heavily different from staging or production; you want it to be optimised for development, maintenance, and bug-fixing, rather than optimised for speed, efficiency, and load.

Ultimately, this means that the build allows you the freedom to write better CSS and JavaScript, in well separated files. These files are then easily maintained by developers—since they’ll be easier to read and well commented—and also easier to maintain through source control—since the separation into a greater number of files means more control of more specific code.