code: https://github.com/webd90kb/webd/tree/master/codes/c_project_template

Creating a well-structured C project can greatly enhance code maintainability, readability, and reusability. This blog post will guide you through a practical example of a C project with a complete directory structure, including a functional Makefile.

Simplified Makefile for Easy Customization

Below is the essential Makefile for our C project. As you can see, it is very simple and easy to modify according to your code structure. The EXE section lists the executable files to be compiled, and the OBJ section contains the shared object files used by multiple executables.

EXE=\
exe1\
a/exe2\
a/exe3\

OBJ=\
mod1\
liba/mod2\
liba/mod3\

include inc.mak

The 'EXE' section lists the executable files that the project will compile. The backslashes (\) allow for multi-line definitions, making it easy to add or remove executables as needed.

The 'OBJ' section lists the object files that are shared across multiple executables. These object files contain common code that can be reused, reducing redundancy and simplifying maintenance.

The 'include inc.mak' line includes the inc.mak file, which contains additional configurations and generic rules for compiling and linking the project. By separating these rules into a different file, the main Makefile remains clean and focused on the specific targets and objects.

By structuring your Makefile in this way, you can easily add or remove executables and object files as your project evolves. This approach not only keeps the Makefile simple but also makes it highly adaptable to various project structures.

Detailed inc.mak for Simplifying the Main Makefile

The inc.mak file contains the more complex rules and settings that allow the main Makefile to remain simple and easy to use. While it includes advanced Makefile syntax, you don't need to fully understand it to benefit from its functionality. Here's a brief overview to help you with potential customization and modifications.

ifneq ($(V), 1)
Q := @
endif

# PREFIX ?= arm-none-eabi-
CC    = $(Q)$(PREFIX)gcc
SIZE    = $(Q)$(PREFIX)size
LD    = $(CC)

CFLAGS = -Wall -Wextra -MMD
LDFLAGS = -Wl,--gc-sections

OBJ:=$(OBJ:=.o)

ifeq ($(OS),Windows_NT)
EXE:=$(EXE:=.exe)
endif

$(EXE): $(OBJ)

all: $(EXE)
.DEFAULT_GOAL := all

ifeq ($(OS),Windows_NT)
$(EXE): %.exe: %.o
    @echo $@
    $(LD) $^ $(LDFLAGS) -o $@
    $(SIZE) $@
else
$(EXE): %: %.o
    @echo $@
    $(LD) $^ $(LDFLAGS) -o $@
    $(SIZE) $@
endif

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


.PHONY:clean
clean:
ifeq ($(OS),Windows_NT)
    @rm -fv $(EXE:.exe=) $(EXE:.exe=.o) $(EXE:.exe=.d)
else #($(OS),Windows_NT)
    @rm -fv $(EXE:=.o) $(EXE:=.d)
endif #($(OS),Windows_NT)
    @rm -fv $(EXE) $(OBJ) $(OBJ:.o=.d)


ifeq ($(OS),Windows_NT)
-include $(EXE:.exe=.d)
else
-include $(EXE:=.d)
endif
-include $(OBJ:.o=.d)

Key Components of inc.mak

  1. Silent Mode Toggle:

     ifneq ($(V), 1)
     Q := @
     endif
    

    This part allows you to toggle the verbosity of the make process. By default, it runs silently.

  2. Compiler and Tools:

     CC    = $(Q)$(PREFIX)gcc
     SIZE    = $(Q)$(PREFIX)size
     LD    = $(CC)
    

    Sets up the compiler (gcc), size utility, and linker. The PREFIX variable can be used to specify a toolchain prefix.

  3. Compiler and Linker Flags:

     CFLAGS = -Wall -Wextra -MMD
     LDFLAGS = -Wl,--gc-sections
    

    Defines the flags for compiling and linking.

    -MMD: This is a GCC-specific flag used to automatically generate .d dependency files. These files contain dependency information that make uses to determine which files need to be recompiled. By using -MMD, you don't have to manually handle dependency tracking, which simplifies the build process and ensures that your dependencies are always up to date.

  4. Object and Executable File Naming:

     OBJ:=$(OBJ:=.o)
    
     ifeq ($(OS),Windows_NT)
     EXE:=$(EXE:=.exe)
     endif
    

    Ensures that object files have a .o extension and, on Windows, executables have a .exe extension.

  5. Build Rules:

     $(EXE): $(OBJ)
    
     all: $(EXE)
     .DEFAULT_GOAL := all
    
     ifeq ($(OS),Windows_NT)
     $(EXE): %.exe: %.o
         @echo $@
         $(LD) $^ $(LDFLAGS) -o $@
         $(SIZE) $@
     else
     $(EXE): %: %.o
         @echo $@
         $(LD) $^ $(LDFLAGS) -o $@
         $(SIZE) $@
     endif
    

    Defines how to build the executables from the object files. There are specific rules for Windows to account for the .exe extension.

  6. Compiling Source Files:

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

    A generic rule for compiling .c files into .o object files.

  7. Clean Target:

     .PHONY:clean
     clean:
     ifeq ($(OS),Windows_NT)
         @rm -fv $(EXE:.exe=) $(EXE:.exe=.o) $(EXE:.exe=.d)
     else
         @rm -fv $(EXE:=.o) $(EXE:=.d)
     endif
         @rm -fv $(EXE) $(OBJ) $(OBJ:.o=.d)
    

    Defines a clean target to remove executables and object files. It includes specific rules for Windows.

  8. Dependency Inclusion:

     ifeq ($(OS),Windows_NT)
     -include $(EXE:.exe=.d)
     else
     -include $(EXE:=.d)
     endif
     -include $(OBJ:.o=.d)
    

    Includes dependency files to manage build dependencies automatically.

By using inc.mak, you can manage complex build rules and configurations separately, keeping the main Makefile clean and straightforward. This setup allows for easy customization and adaptation to different project needs.