Legacy Tools in Modern Stacks Part I: Make and Makefiles, Use Cases Beyond Compiling

Yann Boisclair-Roy
SSENSE-TECH
Published in
7 min readJun 20, 2019

*Click here for Part II and here for Part III

Image Source

make is a Unix tool that simplifies building executable programs composed of multiple independent source files or multiple simultaneous programs. It is able to determine when source files have changed and rebuild only the components needed for an update. make was first developed in 1976, and yet it’s still widely used, predominantly in application development where you need to compile binaries. While makefiles are quite uncommon in Web development in general, at SSENSE we have found an unconventional use-case for it. In this three-part series, we will walk you through how we were able to find a niche for make and makefiles in the modern web development ecosystem. We will explore ways of integrating modern technologies like Docker with the old guard of aliases and makefiles to create decentralized, consistent, and up-to-date local tools without installing any language specific dependencies.

This tutorial will review the basics of a makefile and how it can be used with the make command. We will not compile C applications, instead we will use the ability that make provides to orchestrate command line executions. This will apply regardless of the operating system or the programming language.

Installation

Linux

Typically, when using Linux you will not need to install makefile as most distributions include it by default. If this is not the case, try one of these commands based on your distribution:

# Ubuntu:
$ sudo apt-get install build-essential
# Alpine:
$ sudo apk add — update make
# CentOS:
$ sudo yum install make

MacOS

You can easily install make by installing XCode. Another way, for newer versions, is to trigger the installation by executing the following command in the terminal:

$ code-select — install

Windows

If you have Visual Studio installed, you can run make commands using the integrated terminal. Otherwise, you will need to install a library like GnuWin or CygWin.

Your First Makefile

Open your terminal, then open your favorite text editor and create a Makefile with the following content:

Hello:
echo “world”

Please note that the echo command needs to be tab indented. If you’re using an Integrated Development Environment (IDE), make sure that the indentation is not a combination of four spaces.

When you call make, it will call the first instruction, known as the target. You should have the following lines displayed:

$ make
echo “world”
world

In the last example, we called a simple target, with a simple rule, without any prerequisites. To summarize a target definition, here’s the syntax:

target: prerequisites
<TAB> (recipe — command line instruction)
<TAB> (recipe — command line instruction)

Here’s an example using a prerequisite:

world: hello
@echo “world”
hello:
@echo “Hello”

$ make
Hello
world

Note that “hello” has been called first since it’s a prerequisite for the “world” target. Also, the @ character indicates that we don’t want to display the instruction that is called.

Calling a Specific Target

From the last example, if you remove the prerequisite hello from the world target, you will see that only the world target will be called. This is because make will always only call the first target of the makefile, unless otherwise specified by changing the default parameters or by specifying a target when calling make.

Example where only the first target is called since nothing else was specified:

World:
@echo “world”
Hello:
@echo “Hello”

$ make
world

Changing the Default Target

If you want to change this behavior, you can always use the following instruction .DEFAULT_GOAL, where you specify which target to call by default:

Example with a specified default target:

.DEFAULT_GOAL := hello
World:
@echo “world”
Hello:
@echo “Hello”
$ make
Hello

* Note that it completely skipped the World target

Specifying a Target

The most common way to call a specific target is to specify it when using the make command, as a parameter: make [target]

Example:

.DEFAULT_GOAL := hello
World:
@echo “world”
Hello:
@echo “Hello”

$ make world
world

Phony Commands

When using make, the name of your command cannot match the name of a folder or file in the same directory as your makefile. Otherwise make will try to build based on that folder or file, and totally bypass your commands. To avoid this behaviour, you will need to use the .PHONY instruction.

Example in a folder with a tests directory:

.PHONY: tests
tests:
@echo “run the tests”

$ make
run the tests

Chaining Different Targets

All targets can be called from other targets, just as if they were functions. Using this feature, you can regroup targets all together and chain them in any way you need.

Example where the first and default target will call the other targets:

all: hello world
world:
@echo “world”
hello:
@echo “Hello”

$ make
Hello world

Variable Declaration

For redundant information, you can always declare variables and use them in different targets.

Example using variables:

greeting_msg := “Hello”
all: hello world
world:
@echo “world”
hello:
@echo ${greeting_msg}

$ make
Hello
world

A More Advanced Scenario with Make Functions

If your application relies on a series of different files, you can declare a variable to which the resulting content of a make function will be assigned.

The following is an example which assumes that there are two JSON files in the current directory called “test.json” and “test2.json”:

FILES =: $(wildcard *.json)
display_json_files:
@echo $(FILES)

$ make
: test.json test2.json

How to Apply All That

Now that we understand how a Makefile works, what can we do with it in a practical setting?

When working in an ecosystem with multiple languages, each with different package managers and versions, it becomes complicated to streamline and standardize processes across the board while ensuring that everything is up-to-date and functional.

This is where make comes into play.

Inevitably, every application ends up with more or less the same set of build steps:

  • Install, build, rebuild the app
  • Lint the code
  • Run the tests
  • Run the coverage report
  • Deploy the app on environment ‘X’

etc.

I know what you’re going to say:

“But there’s npm for NodeJS, composer for PHP, pip for Python and Maven for Java, to name just a few. Why would I need make?”

make offers you the ability to standardize how you manipulate your applications across their entire life cycles. When you have multiple ways of doing things, it becomes increasingly complicated to manage processes as new services are added to the stack, using new up-to-date version of their package managers, or even new tools that you now have to support. Additionally, by virtue of using the right tool for every problem, when you add a new language to the stack, you have to support new package managers as well. Given the ambitious architectural challenges we have undertaken at SSENSE, we needed a scalable way to face this type of problem, and make proved itself to be a practical and efficient option.

Defining such a standard is also ideal for DevOps operations, where you outline a specific set of high-level actions (targets) for which each application will create its own specific commands. With this idea, you can more easily define a standard template for pretty much any type of operation such as continuous integration and deployment, end-to-end testing, security checks, etc.

It also helps developers feel more comfortable when moving from one application to another since they already know the high-level toolbox of commands to use, no matter their operating system, and no matter the language of the application.

Another advantage is that you will typically require less documentation updates when you work on any of the high level commands listed in the Makefile. If for example, you’re adding a new kind of test for your app, or you update a shortcut command in the Makefile, you typically don’t need to update the general documentation where it says: “To run the tests, use ‘make tests’”, since you’re changing the underlying code.

Shortcuts

As developers, it’s our job to be lazy… but in an efficient way! If there are long and repetitive commands that people have to run, it’s a good idea to create a make shortcut executing those commands for you. This way, it’s shared with everyone, and can also be used anywhere needed.

Here are a few examples:

  • make review: Run all the tests, linting, coverage, and any other quick checks before committing. This way, you make sure to commit code that respects the quality standards.
  • make dev-install: Install the application locally and install any dev-dependencies that could be useful.
  • make publish [version]: Calls the following commands: docker build, docker tag, and docker push with the given version.

In conclusion, I hope this tutorial will help you in your day-to-day, and you can now see why make isn’t only just for compiling.

This is part one of a three-part series wherein we will review Make, Alias, and Docker, as well as explore ways of creating local tools that all developers can easily contribute to without installing any dependencies.

Sources:

Editorial reviews by Deanna Chow, Liela Touré & Prateek Sanyal

Want to work with us? Click here to see all open positions at SSENSE!

--

--