Make

Contents
Introduction
Installation
Version
Makefile Purpose
Syntax
.PHONY:
Example from C++
Variables
Docker with Makefile
Parameters
BUILD_ID
USER_ID
Alternatives
$$: Calling bash commands (e.g. whoami)
-: Ignore erorrs
Related Topics

Install make

sudo apt install make

or in rpm

sudo yum install make

make can be installed as a part of build-essentials

sudo apt install build-essentials

Check version

/usr/bin/make --version

GNU Make 4.2.1 Built for x86_64-pc-linux-gnu Copyright (C) 1988-2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.

How Makefiles are typically used

Makefiles are used to reduce the number of useless compillations

Typically this applies to C or C++ .

Other programming languages have their own tools used for the same purpose as Make.

Make can be used not only for compiling but also as a tool tracking updates in files or as a set of commands organized in named groups

In this article you can read about C/C++ compilation and about running multiple bash commands in a more or less organized way.

Below you can see a dependency graph for some project.

If some of the dependencies change - make will trigger recomplation.

Dependency graph for compilation image from www.aredel.com
Dependency graph
wikipedia.org

Syntax

Makefile is made from rules.

First is target, then dependencies (prerequisites) and then recipe - set of actions/commands that should be executed

Dependencies are not always needed. For starters we can considre them as files that make should track. When something is updated new compilation should be triggered.

Indentation should be made with TAB. If you do not like to make indentations with TAB - you can set your own option in .RECIPEPREFIX

target: prerequisites recipe

Typical usage: one or more prerequisites is changed → receipe is executed and target file is created.

output: main.o message.o g++ main.o message.o -o output clean: rm *.o output

As in the previous article Configure, make, install in these example we use stadart targets

You are welcome to read about -o и -c options in a dedicated article «Compiling in C++

Option Purpose
-c Compile or assemble the source files, but do not link. The linking stage simply is not done. The ultimate output is in the form of an object file for each source file.

By default, the object file name for a source file is made by replacing the suffix .c, .i, .s, etc., with .o.

Unrecognized input files, not requiring compilation or assembly, are ignored.

man
-o filename Place output in file file. This applies to whatever sort of output is being produced, whether it be an executable file, an object file, an assembler file or preprocessed C code.

If -o is not specified, the default is to put an executable file in a.out, the object file for source.suffix in source.o, its assembler file in source.s, a precompiled header file in source.suffix.gch, and all preprocessed C source on standard output.
-S Directs the compiler to produce an assembly source file but not to assemble the program.

Further reading: gnu.org: Rule-Introduction

If you do not want any files to be created you can you special .PHONY targets

.PHONY

PHONY is one of Special Built-in Target Names

Let take a look on the following Makefile

.PHONY: site site: echo "HeiHei.ru"

Now if you execute

make site

The output will be

echo "HeiHei.ru"
HeiHei.ru

Remove site from the first line and do not touch the receipe

make site

echo "HeiHei.ru"
HeiHei.ru

Nothing is changed but if you create file site in the same directory with Makefile and execute target again - it will not work

touch site
make site

make: 'site' is up to date.

Now target site is reall and as make did not find changed there it did nothing.

Because of this simple coincidence of target name and file name scrtip can stop working.

PHONY can prevent this from happening

Sometimes PHONY is handy because it is possible to list all targest in the beginning of the file.

Example from C++

Let consider example from header files .h article

There are three files

ls

Functions.cpp Functions.h Main.cpp

Main.cpp

#include <iostream> #include "Functions.h" int main() { double b = add(1.3, 4.5); cout << "1.3 + 4.5 is " << b << "\n"; return 0; }

Functions.cpp

double add(double x, double y) { return x + y; }

Functions.h

#pragma once double add(double x, double y);

If one of those files is updated - project should be recompiled. Let' consider the following command

g++ -o output Main.cpp Functions.cpp

First it will compile then link

Let's creat a new Makefile and open it in a text editor, e.g. in Vim

touch Makefile
vi Makefile

Add the following code to the Makefile

output: Main.cpp Functions.cpp Functions.h g++ -o output Main.cpp Functions.cpp

To trigger compilation it is enough to execute

make output

or simply

make

As a result we will get the output file

Next actions to be added are: performing compilation separately and cleaning the directory.

If you have difficulties in understanding what is going on the «C++ compilation» article may help.

.PHONY: clean output: Main.o Functions.o g++ Main.o Functions.o -o output Main.o: Main.cpp g++ -c Main.cpp Functions.o: Functions.cpp g++ -c Functions.cpp clean: rm *.o output

To start script execute

make

g++ -c Main.cpp
g++ -c Functions.cpp
g++ -o output Main.o Functions.o

If you need only to compile Main execute

make Main.o

g++ -c Main.cpp

ls

In this case Main.o will be created but others (Functions.o, output) will not be created

Functions.cpp Functions.h Main.cpp Main.o Makefile

After executing make Main.o we can get a hint on why the term target is used in Makefiles

make Main.o is a command that aims to create a Main.o file an the receipe in Makefile defines a way how to do it.

If you execute make again

make

Main.o will not be recompiled. Only second and third steps wil be executed

g++ -c Functions.cpp
g++ -o output Main.o Functions.o

Execute make if you did not executed it yet and do not execute clean.

Let's add one more function to our project. We should define it in Functions.* files

We will not call it yet so no need to change Main.cpp

Functions.cpp

bool test(bool x) { return x; }

Functions.h

bool test(bool x);

make

g++ -c Functions.cpp g++ -o output Main.o Functions.o

Note that Main.cpp was not recompiled because it was not updated.

Also not the modification fime of the output file - it should change.

Do not change anything and execute

make

make: 'output' is up to date.

There are no changes so no need to recompile.

Variables

.PHONY: clean objects = Main.o Functions.o output: $(objects) g++ -o output $(objects) Main.o: Main.cpp g++ -c Main.cpp Functions.o: Functions.cpp g++ -c Functions.cpp clean: rm *.o output

Run Docker container from Makefile

There is an intro article

«Introduction to Docker» if you are interested.

Example

.PHONY: docker docker: docker-compose -f docker/dev/docker-compose.yml build

Parameters

?= Allows variable to be initialized with existing environment variables value

:= changes variable value

PROJECT_NAME ?= myproject ORG_NAME ?= heihei REPO_NAME ?= myproject #Filenames DEV_COMPOSE_FILE := docker/dev/docker-compose.yml REL_COMPOSE_FILE := docker/release/docker-compose.yml .PHONY: test release test: docker-compose -f $(DEV_COMPOSE_FILE) build docker-compose -f $(DEV_COMPOSE_FILE) up agent docker-compose -f $(DEV_COMPOSE_FILE) up test release: docker-compose -f $(REL_COMPOSE_FILE) build docker-compose -f $(REL_COMPOSE_FILE) up agent docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py collectstatic --noinput docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py migrate --noinput docker-compose -f $(REL_COMPOSE_FILE) up test clean: docker-compose -f $(DEV_COMPOSE_FILE) kill docker-compose -f $(DEV_COMPOSE_FILE) rm -f docker-compose -f $(REL_COMPOSE_FILE) kill docker-compose -f $(DEV_COMPOSE_FILE) rm -f

BUILD_ID

It is common practice to use BUILD_ID to make variable unique

# Docker Compose Project Names REL_PROJECT := $(PROJECT_NAME)$(BUILD_ID) DEV_PROJECT := $(REL_PROJECT)dev

USER_ID

To get ID of the user who executed GNUmakefile you can use USER_ID

USER_ID = $(shell id -u ${USER})

Alternatives to Make

Populat builders for C/C++ are SCons, CMake, Bazel и Ninja. Some IDEs like Microsoft Visual Studio , have their own builders

In Java there are Ant, Maven and Gradle.

Other languages such as Go and Rust, have their own tools

Interpreted languages such as Python , Ruby or JavaScript , do not need direct analog for make.

When script is started it uses the last version of code and that is it.

Meaning of cc -c

cc is a C compiler

There are several commonly available C compilers

In this article gcc was used

-c is here

whoami

In the common Bash script it is enough to run $(whoami) and it is equeal to bash whoami command

Most likely it will not work in your makefile. There are two ways to overcome this issue:

`whoami`

and

$$(whoami)

Ignore errors

If there is an error in the receipe the later commands will not be executed.

Let's consider an example

RPM_DIR=/home/$$(whoami)/rpms/ .PHONY: clean-repo clean-repo: @sudo rm $(RPM_DIR)release/* @sudo rm $(RPM_DIR)master/*

If there is nothing in …release/ make will not even try to delete …master/

There will be an error instead:

sudo rm /home/$(whoami)/rpms/release/* rm: cannot remove ‘/home/andrei/rpms/release/*’: No such file or directory make: *** [clean-repo] Error 1

Using - in from of the command will allow make to ignore an error

RPM_DIR=/home/$$(whoami)/rpms/ .PHONY: clean-repo clean-repo: @-sudo rm $(RPM_DIR)release/* @-sudo rm $(RPM_DIR)master/*

[andrei@localhost ~]$ make clean-repo

rm: cannot remove ‘/home/andrei/Downloads/privx/release_rpms/*’: No such file or directory
make: [clean-repo] Error 1 (ignored)

make complains but moves forward and cleans the master directory

Related Articles
make
Linux
Bash
C
C++
C++ Header files
Configure make install
DevOps
Docker
OpenBSD
Errors make