If you've never had to migrate a project from one build system to another, I envy you. How sweet it is to have not experienced the psychological torture that is unwinding years of hacks and work-arounds layered on-top of one another during a legacy project's lifetime. But nothing lasts forever. You too will know this feeling eventually. Or maybe not. Let's talk about how you can avoid this fate by using a new (very old) build system called Make.

The Case Against Modern Build Systems

Modern build systems use an insane number of dependencies:

  • gulp - 296 nodes, 513 links
  • grunt - 170 nodes, 277 links
  • webpack - 82 nodes, 119 links

gulp (left), grunt (center), webpack (right)

Of the build systems mentioned above, only one is still under active development. That one is webpack, which is used by the popular web framework React. If you are unfamiliar with webpack, good for you. It is a gigantic piece of software that does too many things. From my experience, webpack can be nice to bootstrap a simple proof-of-concept project, but eventually you will hit a wall where you need to do some extremely hacky work-arounds to do something that it doesn't easily support.

Why am I talking about dependencies? Well...

Earlier this week, many npm users suffered a disruption when a package that many projects depend on — directly or indirectly — was unpublished by its author, as part of a dispute over a package name. The event generated a lot of attention and raised many concerns, because of the scale of disruption, the circumstances that led to this dispute, and the actions npm, Inc. took in response.

This was the "leftpad incident" as it has become to be known.

The package author mentioned in the above quote unpublished more than 250 packages from the npm registry in a very short time. This broke thousands of projects and caused a lot of headaches for maintainers and developers throughout the ecosystem.

This alone should be a strong reason to try to limit your project's exposure to huge dependency graphs.

But in case you are not yet convinced, here are a few more reasons:

  • Less time wasted fixing the build process after upgrading dependencies.
  • Reduce the attack surface that could allow malicious/rogue dependencies to:
    • Exfiltrate sensitive data such as keys or secrets via the file system or environment variables.
    • Utilize (abuse) system resources to mine cryptocurrencies.
    • Use system's network capacity to spam, run proxy servers, or DOS attack other services.

The Case For Make

Make. Is. Everywhere. You very likely already have it installed on your system. Or if not, it will be available to install via your system's package repository.

Make is ideal for running builds or as a general purpose task runner. It allows you to easily incorporate bash commands and tools that already exist on your system. All of these tools have been around forever, are well tested, and they are stable.

Make In Practice

Here is an example Makefile that includes comments that explain each section:

## Usage
#
#   $ make build        # compile files that need compiling
#   $ make clean        # remove build files
#   $ make clean build  # remove build files and recompile build files from scratch
#

## Variables
BUILD=build
ALL_CSS=$(BUILD)/css/all.css
SRC=src

# Targets
#
# The format goes:
#
#   target: list of dependencies
#     commands to build target
#
# If something isn't re-compiling double-check the changed file is in the
# target's dependencies list.

# Phony targets - these are for when the target-side of a definition
# (such as "build" below) isn't a file but instead a just label. Declaring
# it as phony ensures that it always run, even if a file by the same name
# exists.
.PHONY: build\
clean\
fonts\
images

build: fonts images $(ALL_CSS)

clean:
  # Delete build files:
  rm -rf $(BUILD)/*

# Define a list of CSS source files.
CSS_FILES=$(SRC)/css/fonts.css\
$(SRC)/css/reset.css\
$(SRC)/css/styles.css\
$(SRC)/css/responsive.css

$(ALL_CSS): $(SRC)/css/
  mkdir -p $$(dirname $@)
  rm -f $(ALL_CSS)
  for file in $(CSS_FILES); do \
    echo "/* $$file */" >> $(ALL_CSS); \
    cat $$file >> $(ALL_CSS); \
    echo "" >> $(ALL_CSS); \
  done

fonts:
  # Copy fonts to build directory.
  mkdir -p $(BUILD)/fonts/OpenSans
  cp -r node_modules/open-sans-fontface/fonts/**/* $(BUILD)/fonts/OpenSans/

images:
  # Copy images to build directory.
  mkdir -p $(BUILD)/images/
  cp -r $(SRC)/* $(BUILD)/images/

It is basically bash with the added syntax for build targets. Make will only build files whose inputs have been modified. So in this example, if you change one of your CSS source files then the all.css build file will be recompiled.

The example above is quite simple. It only includes copying and concatenating files. You can add dependencies as you need them to perform minification of JavaScript files, syntax highlighting, templating, and more.

For more advanced build processes, it's a good idea to execute bash (or node.js) scripts from within the Makefile. This gives you the structure and functionality of Make with the flexibility of whichever scripting language you prefer.

During the last few years, I've migrated several projects to Make and it has turned out to be a great move for the long-term maintainability of those projects. You can have a look at some of these projects for more complex, real-world examples using Make:

  • PayNoWay - Bitcoin double-spending app for Android
  • Bleskomat - Lightning Network ATM hardware + software project

Choose Make as your next project's build system. Your future self will thank you!