A simple Makefile

A Makefile is a special file, commonly used in software development, that controls the build process of a project. It's used with the make utility, a standard build automation tool. Here's why Makefiles are important and what they do:

Purpose of a Makefile

  1. Automating Compilation: A Makefile automates the compilation process, reducing the need to manually compile each source file. This is especially useful in large projects with multiple files.

  2. Efficient Building: It checks the timestamps of source files to recompile only those files that have changed since the last build. This makes the build process faster and more efficient.

  3. Managing Dependencies: It keeps track of dependencies between files. For instance, if a header file is updated, the Makefile ensures that all source files including this header are recompiled.

  4. Custom Build Rules: You can define custom build rules and commands, including how to compile and link the application, how to clean up build artifacts, and other custom tasks.

  5. Platform and Configuration Flexibility: Makefiles can be configured to work on different platforms and environments, allowing for flexible builds under various configurations.

  6. Reproducibility: Ensures that the build process is reproducible. Anyone with the Makefile and source code can build the project in the same way.

Structure of a Makefile

A typical Makefile contains:

  • Targets: These are the names of the actions to be performed, such as build, clean, etc.

  • Prerequisites: Files that must be present/updated before a target is executed, such as source files or headers.

  • Recipes: Commands to achieve a target. They are executed when a target's prerequisites are newer than the target.

Listed below is a basic Makefile that removes the produced outputs and builds a C program with several source files. The following files need be present in your project for this example to work:

  • main.c

  • functions.c

  • functions.h

Makefile:

# Define the compiler
CC=gcc

# Define any compile-time flags
CFLAGS=-Wall -g

# Define any directories containing header files other than /usr/include
#
# Example:
# INCLUDES=-I/home/newhall/include  -I../include
INCLUDES=

# Define library paths in addition to /usr/lib
#   if I wanted to include libraries not in /usr/lib I'd specify
#   their path using -Lpath, something like:
#
# LFLAGS=-L/home/newhall/lib  -L../lib
LFLAGS=

# Define any libraries to link into executable:
#   if I want to link in libraries (libx.so or libx.a) I use the -llibname 
#   option, something like (this will link in libmylib.so and libm.so:
#
# LIBS=-lmylib -lm
LIBS=

# Define the C source files
SRCS=main.c functions.c

# Define the C object files 
#
# This uses Suffix Replacement within a macro:
#   $(name:string1=string2)
#   For each word in 'name' replace 'string1' with 'string2'
# Below we are replacing the suffix .c of all words in the macro SRCS
# with the .o suffix
#
OBJS=$(SRCS:.c=.o)

# Define the executable file 
MAIN=myprogram

#
# The following part of the makefile is generic; it can be used to 
# build any executable just by changing the definitions above and by
# deleting dependencies appended to the file from 'make depend'
#

.PHONY: depend clean

all:    $(MAIN)
    @echo  Simple compiler named myprogram has been compiled

$(MAIN): $(OBJS) 
    $(CC) $(CFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS)

# This is a suffix replacement rule for building .o's from .c's
# It uses automatic variables $<: the name of the prerequisite of 
# the rule(a .c file) and $@: the name of the target of the rule (a .o file)
# (see the gnu make manual section about automatic variables)
.c.o:
    $(CC) $(CFLAGS) $(INCLUDES) -c $<  -o $@

clean:
    $(RM) *.o *~ $(MAIN)

depend: $(SRCS)
    makedepend $(INCLUDES) $^

# DO NOT DELETE THIS LINE -- make depend needs it

This Makefile is a well-structured and commented example for compiling a C program. Let's break down its components and explain each part:

Variable Definitions

  • CC=gcc: This sets the CC variable to the GNU Compiler Collection (gcc), which is the compiler used for building the program.

  • CFLAGS=-Wall -g: These are the flags passed to the compiler. -Wall enables all compiler's warning messages. -g adds debugging information to the executable.

  • INCLUDES=: This would be used to specify additional directories where the compiler can look for header files. It's left empty in this case.

  • LFLAGS=: Used for specifying library paths other than the standard /usr/lib. It's empty here.

  • LIBS=: Specifies any libraries to link against. It's empty, meaning no additional libraries are being linked.

  • SRCS=main.c functions.c: Lists the source files for the project.

  • OBJS=$(SRCS:.c=.o): Converts the .c files listed in SRCS to .o (object) files. This is done using suffix replacement.

  • MAIN=myprogram: Defines the name of the final executable file.

Target Definitions

  • .PHONY: depend clean: Declares depend and clean as phony targets. Phony targets do not represent files; they are just actions.

  • all: $(MAIN): The default target. This is what gets built when you just type make. It depends on the $(MAIN) target (the executable).

  • $(MAIN): $(OBJS): Compiles the executable. This rule says that $(MAIN) depends on $(OBJS) (object files). The command to compile them is given.

  • .c.o:: A rule for converting .c files to .o files. It uses automatic variables ($< for the source .c file, and $@ for the target .o file).

  • clean:: This target removes all the object files and the executable, cleaning up the directory.

  • depend: $(SRCS): Generates a list of dependencies for the source files.

Commands

  • $(CC) $(CFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS): The command to link the object files into the final executable. It uses the variables defined at the top.

  • $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@: The command to compile .c files into .o files.

Comments

  • The Makefile contains detailed comments explaining each part, which is helpful for understanding and modifying the file.

Dependency Generation

  • makedepend $(INCLUDES) $^: makedepend is a tool that automatically generates dependencies of source files. This is useful to ensure that make recompiles files when their dependencies change. $^ is an automatic variable that stands for all the prerequisites of the rule.

End of Dependency List

  • # DO NOT DELETE THIS LINE -- make depend needs it: This is a marker used by makedepend. Dependencies are listed after this line.

In summary, this Makefile is set up to compile a project with main.c and functions.c into an executable named myprogram. It includes comprehensive comments and structure for easy modification and is set up to handle dependencies, compilation, and linking in a clean and organized manner.

Just type make into the terminal while in the directory where the Makefile is located, along with your source files, to utilize this Makefile. The myprogram executable will be created by compiling the sources into object files and linking them.

Run make clean to remove the executable and all object files created during the build.

To ensure that the Makefile is aware of which.o files require recompilation in response to changes to header files, the depend target is utilized to automatically generate dependencies for your source files. To handle dependencies manually or with the makedepend tool, you'll need to install it.

Make sure to use tabs instead of spaces when placing commands under targets in a Makefile.

0
Subscribe to my newsletter

Read articles from Jyotiprakash Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jyotiprakash Mishra
Jyotiprakash Mishra

I am Jyotiprakash, a deeply driven computer systems engineer, software developer, teacher, and philosopher. With a decade of professional experience, I have contributed to various cutting-edge software products in network security, mobile apps, and healthcare software at renowned companies like Oracle, Yahoo, and Epic. My academic journey has taken me to prestigious institutions such as the University of Wisconsin-Madison and BITS Pilani in India, where I consistently ranked among the top of my class. At my core, I am a computer enthusiast with a profound interest in understanding the intricacies of computer programming. My skills are not limited to application programming in Java; I have also delved deeply into computer hardware, learning about various architectures, low-level assembly programming, Linux kernel implementation, and writing device drivers. The contributions of Linus Torvalds, Ken Thompson, and Dennis Ritchie—who revolutionized the computer industry—inspire me. I believe that real contributions to computer science are made by mastering all levels of abstraction and understanding systems inside out. In addition to my professional pursuits, I am passionate about teaching and sharing knowledge. I have spent two years as a teaching assistant at UW Madison, where I taught complex concepts in operating systems, computer graphics, and data structures to both graduate and undergraduate students. Currently, I am an assistant professor at KIIT, Bhubaneswar, where I continue to teach computer science to undergraduate and graduate students. I am also working on writing a few free books on systems programming, as I believe in freely sharing knowledge to empower others.