Can Programmers Who Don’t Understand Makefile Still Write Code?

Introduction: Tired of Typing Compilation Commands? It’s Time to Learn Automation!

Hello everyone, I am Xiaokang!

Last time we talked about GCC, G++, and GDB, clarifying how these tools are used in C/C++ development. With them, you can happily compile and debug your code.

But let me ask you, do you manually type the <span>gcc</span> command every time you compile a project? As the number of project files increases, the command becomes like a tongue twister—long, complex, and prone to errors. Don’t worry, today I will introduce you to a “lazy person’s tool”—Makefile.

The benefits of using Makefile are simple:

  • Automated code compilation, easy and efficient;
  • No need to manually type commands, reducing mistakes;
  • No matter how large the project is, it can handle it all.

Once you learn to write Makefile, you won’t have to worry about the tedious task of compilation anymore! Let’s start from scratch and step by step clarify what it is and how to write it, so you can use it after reading!

1. What is Makefile?

Makefile is a compilation commander, where you write the compilation rules inside, and then with a simple command <span>make</span>, it will automatically complete all compilation tasks according to the rules.

For example, you are the project manager, and Makefile is your notebook, recording the project’s “construction plan”:

  • The source of each target (like the executable file <span>main</span>);
  • What commands are needed to generate these targets;
  • What parts need to be reused (like intermediate files <span>*.o</span>).

In short: Makefile helps you automate the tedious compilation processes!

2. Why Use Makefile?

Suppose you have two source files:<span>main.c</span> and <span>utils.c</span>, the manual compilation steps are roughly as follows:

  1. First, compile <span>main.c</span> and <span>utils.c</span> into object files:
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o

2. Then link the object files into an executable file:

gcc main.o utils.o -o main

It looks simple, but as the code increases, the command becomes like this:

gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc -c file3.c -o file3.o
...
gcc file1.o file2.o file3.o -o my_program

Every extra command typed is another chance to make a mistake; if you change the code, you have to recompile everything, wasting time. With Makefile, you only need:

make

One command, and it’s all done! Plus, it will only compile the modified files, significantly improving efficiency.

Simply put:

  • No Makefile: manually typing commands is tiring.
  • With Makefile: just one command <span>make</span>, and everything else is done automatically, which is great.

3. Basic Structure of Makefile (Understand in One Minute)

Makefile consists of a set of rules, each rule contains three parts:

  1. Target: the file you want to generate, such as <span>main</span>.
  2. Dependencies: what source files or header files the target file needs.
  3. Commands: the commands to run to generate the target.

For example:

main: main.o utils.o
 gcc main.o utils.o -o main

main.o: main.c
 gcc -c main.c

utils.o: utils.c
 gcc -c utils.c

What does this mean?

There are a total of 3 rules above, let’s discuss the first rule:

  1. The target is <span>main</span>, indicating that we want to generate an executable file named <span>main</span>.
  2. The dependencies are <span>main.o</span> and <span>utils.o</span>, meaning that to generate <span>main</span>, these two dependency files must be generated first, and these two dependencies are generated using rules 2 and 3.
  3. The command is <span>gcc main.o utils.o -o main</span>, which is responsible for compiling the <span>.o</span> files into the final executable file.

The last two rules are similar, telling <span>make</span> how to generate <span>main.o</span> and <span>utils.o</span>.

Is it simple? This is equivalent to telling Makefile: “You need to prepare <span>main.o</span> and <span>utils.o</span>, and then use gcc to link them.”

4. Basic Functions of Makefile: Start Automating Compilation Here

4.1 Automatically Generate Object Files

If you manually write the generation rules for <span>main.o</span> and <span>utils.o</span> like above every time, Makefile will become very cumbersome and repetitive. The good news is that Makefile supports wildcards and can automatically generate rules!

%.o: %.c
 gcc -c $< -o $@

How to use this code? Suppose you have <span>main.c</span> and <span>utils.c</span>, Makefile will automatically generate the corresponding rules:

  • <span>main.o</span>: generated from <span>main.c</span>, command is <span>gcc -c main.c -o main.o</span>;
  • <span>utils.o</span>: generated from <span>utils.c</span>, command is <span>gcc -c utils.c -o utils.o</span>.

Explanation of symbols:

  • <span>%.o</span> and <span>%.c</span>: <span>%</span> is a wildcard, indicating file name matching, for example, <span>main.o</span> and <span>main.c</span>.
  • <span>$<span></span></span>: dependency file, for example, <span>main.c</span>.
  • <span>$@</span>: target file, for example, <span>main.o</span>.

With this rule, Makefile directly helps you generate all target files, isn’t that nice?

And as the number of project files increases, the advantages of using Makefile become even more significant.

4.2 Incremental Compilation: Only Compile Modified Files

Makefile has a fantastic feature:only compile the files that need to be updated. It checks the dependency files of each target, and if the dependency files have not changed, it skips the compilation.

For example, if you modify <span>main.c</span>, Makefile will only regenerate <span>main.o</span>, while <span>utils.o</span> remains unchanged. This feature can save a lot of time when there are many project files.

4.3 Clean Up Temporary Files

After compilation, many <span>.o</span> files and intermediate files will be left behind. Makefile can add a <span>clean</span> rule to help you clean up with one command:

clean:
 rm -f *.o main

Just run <span>make clean</span>, and it will be clean and tidy!

5. Advanced Uses of Makefile

After understanding the basic usage, let’s look at some advanced features that can improve development efficiency.

5.1 Basic Usage – Improve Readability and Maintainability

1. Use Variables: Make Makefile More Concise

How to use variables? For example, <span>CC = gcc</span>. Variables can make Makefile more flexible and easier to maintain.

Basic usage of variables:

# Define variables
CC = gcc
CFLAGS = -Wall -g
TARGET = main
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) 

# $(SRCS:.c=.o) is a variable replacement in Makefile, which replaces each .c file name in the variable SRCS with the corresponding .o file name.
# After replacement, OBJS = main.o utils.o

When using variables in commands, you need to reference them in the form of <span>$()</span><span>:</span>

$(TARGET): $(OBJS)
 $(CC) $(CFLAGS) $(OBJS) -o $(TARGET)

# The rules after replacing variables with $() are as follows:
main: main.o utils.o
 gcc -Wall -g main.o utils.o -o main

In this way, if you want to change the compiler or optimization options, you only need to modify the variable part without manually changing each rule.

2. Phony Targets: Make Makefile More Flexible

In Makefile, some targets (like <span>clean</span>) do not generate files but are used to execute specific commands, such as cleaning up temporary files. These targets are called phony targets.

The problem arises: if there happens to be a file named <span>clean</span> in the directory, when running <span>make clean</span>, Makefile will mistakenly think that this file already exists, causing the rule not to execute.

How to solve it?

Use <span>.PHONY</span> to declare phony targets, telling <span>make</span> that this target is not a file and should directly execute the command.

Example: Declare a phony target

.PHONY: clean 
clean:
 rm -f $(OBJS) $(TARGET)

In this way, even if there is a file named <span>clean</span> in the directory, <span>make clean</span> will still execute according to the rules, deleting target files and intermediate files.

Remember: Any target that does not generate a file should be declared with <span>.PHONY</span>!

5.2 Advanced Usage – Build More Powerful Makefile

1. Pattern Rules: Adapt to More File Types

Sometimes our project contains not only <span>.c</span> files but also <span>.cpp</span> files. If we have to write rules separately, it would be too troublesome! At this time, pattern rules can be very helpful.

What are pattern rules?

Pattern rules are a type of generic rule used to tell Makefile:“When encountering this type of file, how to handle it.” For example, tell Makefile:

  • <span>.c</span> files are compiled with <span>gcc</span>;
  • <span>.cpp</span> files are compiled with <span>g++</span>.

In this way, Makefile will automatically select the correct rule based on the file extension, without you having to write them one by one.

How to use? Support C++ files

Suppose there are <span>.cpp</span> files in the project, we can add a pattern rule:

%.o: %.cpp
 g++ -c $< -o $@

In this way, Makefile will automatically compile all <span>.cpp</span> files into <span>.o</span> files, without you having to worry about it.

How to support both C and C++ files?

If the project contains both <span>.c</span> files and <span>.cpp</span> files, we can write two rules:

%.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

%.o: %.cpp
 g++ -c $< -o $@

The function of these two rules:

  1. The first rule: tells Makefile that <span>.c</span> files are compiled with <span>gcc</span>.
  2. The second rule: tells Makefile that <span>.cpp</span> files are compiled with <span>g++</span>.

In this way, whether your files are <span>.c</span> or <span>.cpp</span>, Makefile will automatically handle it.

To summarize:

  • It will automatically select the appropriate compilation method based on the file type;
  • You only need to write one rule, and Makefile can handle a large number of files;
  • No need to write rules repeatedly, saving time and improving efficiency!

Remember: Different file extensions? Use pattern rules to handle them all!

2. Conditional Statements: Make Makefile Smarter

Conditional statements can allow Makefile to adjust rules based on actual situations, such as different operating systems or different compilation modes, making it both flexible and worry-free.

1. Adapt to Different Platforms

The commands for different operating systems may vary, for example, deleting files, Linux uses <span>rm</span>, while Windows uses <span>del</span>. Through conditional statements, Makefile can automatically select the correct command:

OS = $(shell uname)

ifeq ($(OS), Linux)
 CLEAN_CMD = rm -f
else
 CLEAN_CMD = del
endif

clean:
 $(CLEAN_CMD) *.o $(TARGET)
  • Running on Linux with <span>make clean</span>: executes <span>rm -f</span>;
  • Running on Windows with <span>make clean</span>: executes <span>del</span>.

In this way, you don’t have to manually change commands on different platforms, which is convenient!

2. Switch Compilation Modes

During development, it is often necessary to switch between debug mode and release mode:

  • Debug mode: includes debugging information (helpful for troubleshooting).
  • Release mode: optimizes performance (suitable for production environments).

Using conditional statements makes it easy to implement:

ifeq ($(MODE), debug)
 CFLAGS = -g -O0
else
 CFLAGS = -O2
endif

all:
 $(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
  • Run in debug mode:
make MODE=debug

Using <span>-g</span> and <span>-O0</span> to compile, generating a program with debugging information.

  • Run in release mode:
make

or:

make MODE=release

Using <span>-O2</span> to compile, generating an optimized high-performance program.

To summarize:

  • Adapt to different platforms: Conditional statements allow Makefile to work on both Linux and Windows.
  • Switch compilation modes: Convenient for debugging during development and optimizing for production environments.

3. Automated Dependency Management: Make Makefile Smarter!

When writing code, <span>.c</span> files often use header files <span>.h</span>. For example, your <span>main.c</span> may contain:

#include "utils.h"

If one day you modify <span>utils.h</span>, how does Makefile know it needs to recompile <span>main.c</span>?

Relying on you to manually write dependency rules? Don’t joke, with many project files, relying on manual dependency writing will exhaust you.

At this time, automated dependency management comes into play.

What is automated dependency management?

  • The core of automated dependency management is using <span>gcc -M</span>, which can help you automatically generate the dependency relationships between <span>.c</span> files and <span>.h</span> files. Every time you modify a header file, Makefile will automatically trigger the related <span>.c</span> files to recompile.

How to write the code?

See the following Makefile example:

# Define the list of dependency files
DEPS = $(SRCS:.c=.d)

# Generate .d files, write dependency rules
%.d: %.c
 $(CC) -M $< > $@

# Include dependency files
include $(DEPS)

What does it do?

  1. Define dependency files
DEPS = $(SRCS:.c=.d)

Replace each <span>.c</span> file in the source file list <span>SRCS</span> with the corresponding <span>.d</span> file, for example:

  • <span>main.c</span><span>main.d</span>
  • <span>utils.c</span><span>utils.d</span>

These <span>.d</span> files are used to record the relationships between <span>.c</span> and <span>.h</span> files.

2. Automatically generate dependency rules

%.d: %.c
 $(CC) -M $< > $@

This rule will use <span>gcc -M</span> to generate a <span>.d</span> file for each <span>.c</span> file, which records what header files it depends on.

For example, if your <span>main.c</span> includes <span>utils.h</span>, the generated <span>main.d</span> file might look like this:

main.o: main.c utils.h

3. Include dependency rules

include $(DEPS)

This line tells Makefile to load the contents of all <span>.d</span> files. Every time Makefile runs, it will check the rules in the <span>.d</span> files to see which files need to be recompiled.

What is the effect?

Suppose you have the following files:

  • <span>main.c</span> depends on <span>utils.h</span>;
  • <span>utils.c</span> does not depend on any header files.

If you modify <span>utils.h</span>, Makefile will automatically discover this change and only recompile <span>main.c</span>, without touching <span>utils.c</span>.

The benefits of automated dependency management:

  1. No more manual writing of dependency rules, making Makefile smarter;
  2. Every time a header file is updated, Makefile automatically determines which files need to be recompiled;
  3. Even if the project files are numerous, it can handle them easily.

Simply remember: “If there are <span>.h</span> files, use <span>gcc -M</span> to automatically generate dependencies!”

4. Multi-target Support: Use Makefile to Manage Multiple Modules

As your project files increase, even dividing into multiple modules, such as <span>lib</span> for core functionality and <span>app</span> for the main program module, relying on a single Makefile becomes very difficult.

At this time, the smart approach is:

  • Each module has its own Makefile, managing its own rules separately;
  • Use a master Makefile to schedule all modules, making the project clearer and more efficient!

Modular approach:

1. Write a separate Makefile for each module

For example, in the <span>lib</span> module directory, we write a <span>lib/Makefile</span>:

# lib/Makefile
lib.a: lib.o               # Define target lib.a
 ar rcs lib.a lib.o       # Package lib.o into static library lib.a

lib.o: lib.c               # Compilation rule: generate lib.o
 gcc -c lib.c -o lib.o
  • lib.a is a static library, <span>ar rcs</span> is the packaging command.
  • This Makefile only concerns the files in the <span>lib</span> module, without affecting other modules.

2. Use the master Makefile to schedule all modules

The master Makefile is located in the project root directory, responsible for stringing together all modules. It does not care about the specific rules of each module but recursively calls each module’s own Makefile:

# Master Makefile
SUBDIRS = lib app          # Define module directories

all: $(SUBDIRS)            # Master target: compile all modules

$(SUBDIRS):                # Recursively call each module's Makefile
 $(MAKE) -C $@

clean:                     # Clean all modules
 for dir in $(SUBDIRS); do $(MAKE) -C $$dir clean; done

Code explanation:

1. SUBDIR is the module list: Here it defines the modules in the project, such as <span>lib</span> and <span>app</span>. Each module directory has its own Makefile.

2. $(MAKE) -C $@ is key: This command means to switch to the specified directory (<span>-C</span>), and then run the Makefile in that directory. For example, <span>$(MAKE) -C lib</span> means to run its Makefile in the <span>lib</span> directory.

3. Recursive cleaning: The clean target will loop into each module directory and call their <span>clean</span> rules. Note that the double <span>$$dir</span> in <span>$$dir</span> is to allow Makefile to parse correctly.

Overall effect:

  • You can run <span>make</span> in the master directory, and it will automatically compile all modules;
  • When running <span>make clean</span>, it will recursively clean the temporary files of all modules;
  • Each module’s rules are independent, clear, and easy to maintain.

The benefits of using a master Makefile to schedule multiple modules:

  1. Clear structure: Each module’s rules are managed independently, and the master Makefile only schedules.
  2. Easy maintenance: When modifying or adding modules, just add the corresponding module directory in <span>SUBDIRS</span>.
  3. Efficient recursion: By calling the subdirectory’s Makefile with <span>$(MAKE) -C</span>, modules do not interfere with each other.

In simple terms: manage by modules, schedule with the master Makefile, and everything is in order!

5.3 Advanced Techniques – Optimize Efficiency and Flexibility

1. Parallel Compilation: Improve Efficiency

The <span>make</span> command in Makefile supports parallel execution of multiple rules, using the <span>-j</span> parameter to specify the number of parallel tasks.

Example: Parallel Compilation

make -j4

This will run up to 4 tasks simultaneously, fully utilizing multi-core CPUs and significantly improving the compilation speed of large projects.

2. Custom Functions: Reuse Logic

When writing Makefile, if there is repeated compilation logic in the rules, such as compiling <span>.c</span> files into <span>.o</span> files, repeatedly writing <span>$(CC) $(CFLAGS)</span> is cumbersome. At this time, we can use custom functions to manage these repetitive operations uniformly, making it convenient and efficient!

Define a function:

Use <span>define</span> and <span>endef</span> to define a compilation function:

define compile
 $(CC) $(CFLAGS) -c $< -o $@
endef
  • <span>compile</span> is the function name, representing the compilation logic;
  • <span>$<span></span></span> is the dependency file (like <span>main.c</span>), and <span>$@</span> is the target file (like <span>main.o</span>).

Using the function:

When calling the custom function, use <span>$(call function_name)</span>:

%.o: %.c
 $(call compile)

This rule will automatically compile <span>.c</span> files into the corresponding <span>.o</span> files.

To summarize:

  1. Custom functions reduce repetitive code;
  2. When modifying logic, you only need to change the function definition, and other places remain unchanged;
  3. Makefile becomes concise and readable, clear and efficient.

In short: encapsulate repetitive logic into functions, and Makefile can also be elegant!

3. Static Pattern Rules: Batch Generate Target Files

When multiple files need to be compiled with similar rules, writing them one by one is too troublesome; using static pattern rules can solve it all at once!

First, let’s look at a simple example: Suppose we want to compile multiple <span>.c</span> files into <span>.o</span> files:

OBJS = main.o utils.o io.o

$(OBJS): %.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

What does this mean?

  • <span>$(OBJS)</span> is the target file list, such as <span>main.o</span>, <span>utils.o</span>;
  • <span>%.o: %.c</span> indicates that each <span>.o</span> file is generated from the corresponding <span>.c</span> file;
  • <span>$<span></span></span> is the source file (like <span>main.c</span>), and <span>$@</span> is the target file (like <span>main.o</span>).

The advantages:

  1. Reduce repetition: one rule handles batch processing, saving time and effort;
  2. Automatic matching: file names automatically correspond, no need to manually write each rule.

By the way, let’s mention pattern rules, these two types of patterns are similar in usage.

For simpler projects, you can use pattern rules to achieve similar effects:

%.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

Explanation:

  • Each <span>.o</span> file is generated from the corresponding <span>.c</span> file;
  • The wildcard <span>%</span> matches any file name, for example, <span>main.c</span> automatically corresponds to <span>main.o</span>.

Static Pattern Rules vs. Pattern Rules

Feature Static Pattern Rules Pattern Rules
Matching Scope For specific target lists (like <span>$(OBJS)</span>) Automatically matches all files that meet <span>%</span> criteria
Flexibility More precise control, only processes specified target files Simple and unified, suitable for global rules
Applicable Scenarios Many files, complex rules, specific files need special handling Few files, unified rules, simple projects

To summarize:

  • Pattern rules are suitable for simple projects, one rule handles all files;
  • Static pattern rules are suitable for complex projects, allowing precise control over which files apply the rules.

Remember: Use pattern rules for simple global handling, and static rules for precise processing!

4. Cross-Platform Builds: Use CMake to Generate Makefile

If the project needs to be compiled on multiple platforms (like Windows, Linux, macOS), writing Makefile directly can be cumbersome. At this time, you can use CMake to automatically generate Makefile that adapts to different platforms.

How to use:

1. Create a CMake Configuration File

Create a new <span>CMakeLists.txt</span> in the project directory, with the following content:

# Declare minimum version requirement
cmake_minimum_required(VERSION 3.10)
# Define project name
project(MyProject)
# Specify executable file
add_executable(main main.c utils.c)

2. Generate Makefile Run in the terminal:

cmake .

3. Compile the Project Use the generated Makefile:

make

Advantages:

  • Cross-platform: adapts to Windows, Linux, macOS, and other operating systems;
  • Simplified Management: no need to manually write complex Makefile.

In summary: Use CMake to automatically generate Makefile, cross-platform compilation is that simple!

6. Complete Example

Combining what we learned earlier, let’s look at a complete Makefile:

# Define variables
CC = gcc                     # Compiler
CFLAGS = -Wall -g            # Compilation parameters: enable all warnings and debugging information
SRCS =  $(wildcard *.c)      # Get all .c files in the current directory and assign them to the SRCS variable, for example: SRCS = main.c utils.c 
OBJS = $(SRCS:.c=.o)         # Replace .c files with .o files, after replacement, OBJS = main.o utils.o 
TARGET = main                # The final executable file to be generated

# Compilation rules
$(TARGET): $(OBJS)
 $(CC) $(CFLAGS) $(OBJS) -o $(TARGET)

# Generate .o file rules
%.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

# Add .PHONY to declare phony targets
.PHONY: clean

clean:
 rm -f $(OBJS) $(TARGET)

Usage:

  • <span>make</span>: generates the executable file <span>main</span>;
  • <span>make clean</span>: will delete all <span>.o</span> files and the executable file <span>main</span>, keeping the project directory clean.

After reading this article, I believe you should find the Makefile code above quite easy!

7. Final Thoughts: Start with Makefile, Move Towards Compilation Automation!

Makefile is the “lazy person’s tool” in compilation; once you use it, you will find:

  • No more manually typing commands, compilation becomes simpler;
  • Even as the project grows larger, managing it is effortless;
  • Improves efficiency, saves time, and easily handles complex compilations!

If you are still manually typing commands, hurry up and try writing a Makefile to experience the joy of automation!

In the next article, we will take you into the world of CMake, learning how to manage projects across platforms, stay tuned!

If this article helped you, remember to give a thumbs up and a like 👍 before you go! If you have any questions, feel free to chat in the comments! You are also welcome to follow my public account “Learn Programming with Xiaokang“, where I will continue to share hardcore technical articles on computer programming!

How to follow my public account?

It’s very simple! Just scan the QR code below to follow.

Can Programmers Who Don't Understand Makefile Still Write Code?

In addition, Xiaokang recently created a technical exchange group specifically for discussing technical issues and answering readers’ questions. If you have any knowledge points you don’t understand while reading the article, feel free to join the exchange group to ask questions. I will do my best to answer everyone. Looking forward to progressing together!

Can Programmers Who Don't Understand Makefile Still Write Code?

Leave a Comment