BACK
Feed

Makefiles for fun and profit

I use a lot of different tech stacks in my day job. When you add in all my hobby projects from the tildeverse to fiction writing, the number of tools and setups is quite silly. Trying to remember how the tools work is a losing battle.

Enter Makefiles!

Lots of articles have been written about using Makefiles to automate your projects. I’m going to share my personal choices and explain my setup.

How I build this blog

My first rule for all of my Makefiles is that the default task is always a phony called “help”. The help task is exactly the same in every one of my Makefiles:

help:
        @echo "targets:"
        @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
        | sed -n 's/^\(.*\): \(.*\)##\(.*\)/  \1|\3/p' \
        | column -t  -s '|'

This task greps through the Makefile itself looking for any targets that have a double-# comment. If so, that target is listed and the comment becomes the help description. (e.g., build: ## build hugo source)

In the case of this blog, that looks like:

targets:
  new      create new post
  serve    start hugo watcher and webserver
  build    build hugo source
  sign     gpg sign blog content
  deploy   send built files to webserver

The majority of my other targets are simply wrappers for the specific commands that apply to any given project. Is it a node thing? There’s probably an NPM command in there, or Yarn. Python? Oh, you better believe I’ve got some virtual env activate/deactivate things in there.

I also commonly have a deploy task, whether that involves triggering a git signing & tagging process, or rsyncing files, who knows. The point is I don’t need to remember. I know what I want to do and now I have a unified way of doing it.

So why make?

It’s there. It works on all the systems I use. It’s not very hard to work with and it has some nice dependency management.

I know what you’re thinking. “You’re not even using the powerful features of make! This might as well be a shell script.” And, in many cases, you’re right. But not all.

On the contrary, I have several projects that use dynamic file-based targets to generate compiled versions of source files with some really cool dependency stuff. I built myself a typescript web environment that outputs raw html, css, and js without any webpack fluff! In the case of this blog, I’m using a bit of make magic to identify the signing files for each of my blogposts.

INDEX_FILES != find public/ -name 'index.html'
SIG_FILES := $(INDEX_FILES:%.html=%.html.asc)
GPG_FINGERPRINT="4E0FEB0E09DDD7DF"

sign: $(SIG_FILES) ## gpg sign blog content

public/%.html.asc: public/%.html
        gpg --batch --yes --local-user $(GPG_FINGERPRINT) --armor --detach-sign $<

It would even be smart enough not to re-generate the signing files for untouched posts, except Hugo is greedy and regenerates the entire site everytime anything is updated. Oh well.

The long and short of it

I use Make to make things easier and simpler. If doing something in make is too hard, I don’t do that. I write a shell script and tell make to call it! Or python, or golang, or node.

Should you use make? Eh, I don’t care. If you have difficulty remembering the ins-and-outs of lots of different setups, then maybe it would be helpful. Or you could use Just or a shell script, or whatever. This isn’t some universal life lesson teaching you the One True Way™. It’s just helpful to me.


Here’s the full Makefile for this blog for reference:

INDEX_FILES != find public/ -name 'index.html'
SIG_FILES := $(INDEX_FILES:%.html=%.html.asc)
GPG_FINGERPRINT="4E0FEB0E09DDD7DF"

help:
        @echo "targets:"
        @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
        | sed -n 's/^\(.*\): \(.*\)##\(.*\)/  \1|\3/p' \
        | column -t  -s '|'

new: ## create new post
        @test -n "$(title)" || read -p "Enter a title for your post: " title; \
        export title_slug=`echo $${title:-Untitled} | sed -E -e 's/[^[:alnum:]]/-/g' -e 's/^-+|-+$$//g' | tr -s '-' | tr A-Z a-z`; \
        export post_path=content/post/$$title_slug.md; \
        echo "Creating $$post_path"; \
        echo "---"                                                  >  $$post_path; \
        echo "date: `date +"%Y-%m-%d %H:%M:%S %z"`"                 >> $$post_path; \
        echo "title: \"$$title\""                                   >> $$post_path; \
        echo "url: \"/$$title_slug\""                               >> $$post_path; \
        echo "tags: "                                               >> $$post_path; \
        echo "  - meta "                                            >> $$post_path; \
        echo "---"                                                  >> $$post_path; \
        echo " "                                                    >> $$post_path; \
        echo " "                                                    >> $$post_path; \
        echo " "                                                    >> $$post_path; \
        echo " "                                                    >> $$post_path; \
        echo "<!--  vim: set shiftwidth=4 tabstop=4 tw=80 expandtab: -->" >> $$post_path; \
        vim $$post_path

serve: ## start hugo watcher and webserver
        hugo server -D

build: ## build hugo source
        hugo --gc --minify

sign: $(SIG_FILES) ## gpg sign blog content

public/%.html.asc: public/%.html
        gpg --batch --yes --local-user $(GPG_FINGERPRINT) --armor --detach-sign $<

deploy: build $(SIG_FILES) ## send built files to webserver
        rsync -rvhe ssh --progress --delete ./public/ labs.tomasino.org:/var/www/labs.tomasino.org/

.PHONY: new serve build deploy help sign

This page is cryptographically signed with my public key. (learn more)