GNU Logo

Spring is in the air again and the young developer finds his old pile of JavaScript just does not want to build anymore. So, in a fit of reason, he decides that old school is new again and that GNU Make at least has good documentation. Yes, you heard correctly, what was once 200 lines of JS configuration is now a 20 line Makefile.

Since Our Last Episode

There have been a few changes in the world of Jekyll since I last really explored setting up an automated build. Better asset handling and the inclusion of gem themes are probably the biggest changes but luckily the Minima theme is easy to customize and the documentation is still quite good.

The world of JavaScript has changed even more, resulting in a Gruntfile that wouldn’t build the site anymore filled with plugins that replicated features Jekyll already had. So…I pitched it. Threw the whole thing out, and started over using GNU Make instead. It might sound crazy, but keep reading, you might end up surprised.

The Make-ings of Greatness

You’re probably thinking, “Make…that sounds familiar. I think I used it once for a project in C or to build a package for my distro, what’s it got to do with a blog?” Simple, it’s a tool that lets you create lists of tasks and execute them based on dependencies. That makes it perfect for building resources and keeping complex tasks in order while you focus on your site’s content. The other benefit is if you’re going for a CI/CD setup, using a Makefile can keep your local and CI build instructions in sync more easily. No need to update the script in both places, just call make build for both!

Starting out from a blank Jekyll site is easy with the Makefile below. You can omit the presentation and clone information if you like but I’ll be using it later to highlight the flexibility of Make.

CURRENT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
PRESENTATION_REPO_URL = git@example.com:thomascfoulds/presentations.git
PRESENTATION_DIR = $(CURRENT_DIR)/assets/presentations
JEKYLL_SERVE_OPTS = --drafts

clone:
ifneq "$(wildcard $(PRESENTATION_DIR)/*)" ""
	cd $(PRESENTATION_DIR) && git pull origin master
else
	git clone $(PRESENTATION_REPO_URL) $(PRESENTATION_DIR)
endif

clean: configure
	bundle exec jekyll clean

configure:
	bundle install --path=vendor
	bundle update

serve: clean
	bundle exec jekyll serve $(JEKYLL_SERVE_OPTS)

build: clean clone
	JEKYLL_ENV=production bundle exec jekyll build

Anatomy of a Makefile

First of all, I should direct you to the very in depth Make documentation which will do a far better job than I will describing all the different options available in Make. In any case, let’s break apart this Makefile into its various parts.

CURRENT_DIR = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
PRESENTATION_REPO_URL = git@example.com:thomascfoulds/presentations.git
PRESENTATION_DIR = $(CURRENT_DIR)/assets/presentations
JEKYLL_SERVE_OPTS = --drafts

Most examples you’ll see open the Makefile with a collection of variables that are used throughout the various tasks. In this case I’m setting up compiler options of Jekyll and setting up some paths for my slideshow repository. You can also reference variables you’ve defined previously using the $(VARIABLE_NAME) syntax to build up longer values out of smaller parts. This can be especially helpful in making your Makefile portable to systems and users other than yourself.

configure:
	bundle install --path=vendor
	bundle update

The task or target is the smallest useful part of a Makefile and describes a series of actions to achieve a goal. The goal can be anything from compiling an executable to making sure your dependencies are up to date like you see here. This target is named configure and would be called from the commandline with make configure. That would install and and update any gems I have listed in my Gemfile while printing the command output to the screen. If either command fails, exiting with a nonzero status, Make is also smart enough to stop running and report the error. While that isn’t super useful for a simple task like this it’s a lifesaver for more complex tasks that are dependant on eachother. You don’t need to worry about a compiler error early on corrupting your final product.

clone:
ifneq "$(wildcard $(PRESENTATION_DIR)/*)" ""
	cd $(PRESENTATION_DIR) && git pull origin master
else
	git clone $(PRESENTATION_REPO_URL) $(PRESENTATION_DIR)
endif

So the first difference you probably noticed between this task and the one before is that some of the commands aren’t indented to the same level. Make uses whitespace to determine what is a command meant for it and which is meant to build the target. It also specifically requires tab, \t, characters for its indentation by default. Spaces may look the same but will confuse Make, so be sure your editor isn’t expanding tabs by default! In this case, we’re using Make to check if a directory has any contents and if not we clone down my collection of presentation slideshows from another Git repo. If it does exist, instead we switch into the directory and pull down the latest commits to the master branch. Now you might wonder why the cd $(PRESENTATION_DIR) is on the same line as everything else. Why not split it into two lines, wouldn’t it be cleaner? Actually, Make executes each line in it’s command list in its own shell context and in the same directory as the Makefile. So to actually be in the right directory to do our Git tasks we have to change directory and run them all on the same line.

clean: configure
	bundle exec jekyll clean

serve: clean
	bundle exec jekyll serve $(JEKYLL_SERVE_OPTS)

build: clean clone
	JEKYLL_ENV=production bundle exec jekyll build

Now we get to see the real power of Make with its dependency handling. Here we have three tasks that all have dependencies defined in <target>: <dependency> format. What this means is that when we run make clean it will look through the Makefile to find the configure task and run it first before trying to run any of the clean commands. Great news because without our gems we’re not going anywhere fast! Multiple dependencies are just as easy, list them out space separated after the target name or put them in a variable and include them in with a $(DEPS).

Building the Site

Putting it all together is the final step to getting your site running smoothly. I like feeling like an 80’s Hollywood hacker so I draft my posts with a combination of tmux and NeoVim in a terminal window which makes having a split running make serve very natural. But if that isn’t your idea of a good time any editor will work and you can keep the terminal window in the background working for you. Once you’re done writing and are ready to push your site to the server it’s time to use our last Make target. Running make build will clean up any leftovers in your _site directory and pull down the latest slideshows before running a production Jekyll build. The JEKYLL_ENV variable can be useful for selectively including additional templates like analytics which you’d rather not deal with in a development environment. Once Make finishes building your site you’re ready to copy your _site directory up to a webserver for the world to see.

What’s Next?

At this point we have a site that builds reliably and a system which will handle dependencies for us as we add more tasks, a great starting point. We’re still missing an asset pipeline for images, a secure webserver, and automated deploys of new content out to the server. Check back soon for how to configure nginx to serve our Jekyll site, optimize images for the web, and set up CI builds to make posting new content just a Git commit away!