Ubuntu Tutorial: Intro to Make

0 - Introduction

Make is a tool mainly used to automate the build process of a project. Mainly used in C and C++, it helps us avoid the need to write 100 different commands in the console just to see our app run.

In this article we will be using gcc and make to show you a little example, you can get a tutorial on how to install gcc here.

1 - Install

We recommend installing make via the ‘build-essential’ package, like did in the gcc article but, if you already have gcc or other compiler installed you can install make with the following command, but before that, update your system:

sudo apt update
sudo apt upgrade
sudo apt install make

1 - Example

Now that we have make installed let’s make a little c program to test it, start by creating the following file structure:

project:
    src:
        mylib:
            - hello.h
            - hello.c
        - main.c
    - makefile

And now the files inside the project:

// main.c
#include "mylib/hello.h"

int main()
{
    printHello();
    return 0;
}
// mylib/hello.h
#ifndef HELLO_H
#define HELLO_H

void printHello();

#endif
// mylib/hello.c
#include <stdio.h>

void printHello()
{
    printf("Hello\n");
}

And at last, the makefile:

TARGET_EXEC := program
BUILD_DIR := ./build
SRC_DIRS := ./src

# compiler flags
CFLAGS := -Wall
CXXFLAGS := -Wall

# finds all C, Cpp and assembly files in the source folder
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c')
# prepends BUILD_DIR and appends .o to every src file
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# every folder in SRC_DIRS will be passed to GCC
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# add prefix to INC_DIRS for them to be parsed by GCC correctly
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# pass includes to linker
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
	$(CXX) $(OBJS) -o $@ $(LDFLAGS)

# Build step for C source
$(BUILD_DIR)/%.c.o: %.c
	mkdir -p $(dir $@)
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Build step for C++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
	mkdir -p $(dir $@)
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

# this line states that run, clean and makedebug are 'functions' and not files
.PHONY: run clean makedebug

# Run program
run: 
	$(BUILD_DIR)/$(TARGET_EXEC)

# Delete build_dir
clean:
	rm -r $(BUILD_DIR)

makedebug:
	# CC $(CC)
	# CXX $(CXX)
	# TARGET_EXEC $(TARGET_EXEC)
	# BUILD_DIR $(BUILD_DIR)
	# SRC_DIRS $(SRC_DIRS)
	# SRCS $(SRCS)
	# OBJS $(OBJS)
	# INC_DIRS $(INC_DIRS)
	# INC_FLAGS $(INC_FLAGS)
	# CFLAGS $(CFLAGS)
	# CXXFLAGS $(CXXFLAGS)
	# CPPFLAGS $(CPPFLAGS)
	# DEPS $(DEPS)

# Include dependencies but suppress warns
-include $(DEPS)

You might be very confused by what this all means, but let’s see it one by one:

  • ‘TARGET_EXEC’ is the name of the compiled program
  • ‘BUILD_DIR’ is the folder where all the ‘.o’ and ‘.d’ files will be stored along with your program
  • ‘SRC_DIRS’ are the folders where the compiler will need to look for source files
  • ‘CFLAGS’ and ‘CXXFLAGS’ are the flags for gcc and g++
  • ‘SRCS’ is every ‘.c’ or ‘.cpp’ file found in ‘SRC_DIRS’
  • ‘OBJS’ is every ‘.o’ file that will be needed for compilation
  • ‘INC_DIRS’ are all the folders that contain source files and will be passed down to gcc
  • ‘INC_FLAGS’ is the same as ‘INC_DIRS’ but formatted in a way the gcc accepts (ex. ‘-I./src’)
  • ‘CPPFLAGS’ are the flags passed to gcc
  • ‘$(BUILD_DIR)/’ are the steps of compilation
    • ‘$(TARGET_EXEC)’ is the final step, linking
    • ‘%.c.o’ is the step that compiles ‘.c’ files to ‘.o’ using gcc
    • ‘%.cpp.o’ is the step that compiles ‘.cpp’ files to ‘.o’ using g++
  • ‘.PHONY’ is a make declaration that let’s you specify that certain declarations are not files
  • ‘run’: this is the same as writing ‘./build/program’, and when you write ‘make run’ in the console, it will start your program
  • ‘clean’: simply deletes the build folder
  • ‘makedebug’: prints the value of all the variables in the makefile
  • ‘-include $(DEPS)’ includes dependencies but suppresses the warning if empty (‘-‘)

And now, how do you use it? It’s simple:

# compile the program
make
# run the program
make run
# delete build folder (program and all .o and .d files)
make clean
# prints all variables in the console
make makedebug

And that’s it, now you just have to code and not think about what flags you need in the compiler or if you forgot any file every time you change a string or number in your program. You can also customize a step for a specific file. For example, if you need a file named ‘print.s’ (an assembly file), you can add a step for that specific file.

Thanks for reading! Stay tuned for more tech insights and tutorials. Until next time, and keep exploring the world of tech!