惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
Hugging Face - Blog
Hugging Face - Blog
博客园 - 聂微东
量子位
S
SegmentFault 最新的问题
V
Visual Studio Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News
D
Docker
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
博客园 - Franky
Recent Announcements
Recent Announcements
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
V
V2EX

Luca Cavallin

AI Engineering for Developers | Blog AI Engineering for Developers Platform Engineering End-to-End | Blog Google Cloud Networking 101: The Comprehensive TLDR | Blog Google Cloud Networking 101: The Comprehensive TLDR Containers Are Not Automatically Secure | Blog Containers Are Not Automatically Secure Watery Stone Beacon | Photography Blue Iceman Suture | Photography Hidden Emerald Pool | Photography Autumn Chapel Pinnacles | Photography A Tour of eBPF in the Linux Kernel: Observability, Security and Networking | Blog A Tour of eBPF in the Linux Kernel: Observability, Security and Networking Shared Violet Pulse | Photography Kubernetes Networking from Packets to Pods | Blog An Overview of Network Protocols | Blog An Overview of Network Protocols A Quick Journey Into the Linux Kernel | Blog A Quick Journey Into the Linux Kernel OpenTelemetry: A Guide to Observability with Go | Blog I'm on the Cillers Podcast Talking About Tech and Hackathons | Blog Yet Another List of Random Opinions on Writing Readable Code and Other Rants | Blog My post about Istio is now on the Istio blog too! | Blog Tropical Jungle Escape | Photography The Istio Service Mesh for People Who Have Stuff to Do | Blog Dreamy Cartoonscape Windmill | Photography Twilight Windmill Reflections | Photography Notes I took while reading "Applied Machine Learning and AI for Engineers" and "Introducing MLOps" | Blog Things I've Learned About Terraform That I Keep Telling People About | Blog Analyzing Unsplash Photo Performance with Python | Blog
Crafting a Clean, Maintainable, and Understandable Makefi...
Luca Cavallin · 2023-10-22 · via Luca Cavallin

In the world of software development, especially in C projects, a Makefile is your blueprint for how your project is built and organized. It's a simple way to manage the build process, dictating how your source code transforms into an executable program. Alternatives include build systems like CMake or Meson, but Makefiles remain a popular choice due to their straightforward nature. In a Makefile, you can print to the screen using the echo command, utilize wildcards to specify groups of files, and employ 'phony' targets to run commands regardless of file dependencies.

Now, diving into my project, I aimed to create a Makefile that is clean, maintainable, and easy to comprehend. I took a systematic approach to achieve this, and the result can be found within the gnaro repository on GitHub.

Variables

makefile

debug ?= 0
NAME := gnaro
SRC_DIR := src
BUILD_DIR := build
INCLUDE_DIR := include
LIB_DIR := lib
TESTS_DIR := tests
BIN_DIR := bin

This section defines general project settings:

  • debug: A conditional flag for building in debug mode. This is used to determine whether to enable debugging flags in the build process.
  • NAME: The name of the project.
  • SRC_DIR to BIN_DIR: Directories for the source files, build output, includes, libraries, tests, and binaries.

Object File Paths

makefile

OBJS := $(patsubst %.c,%.o, $(wildcard $(SRC_DIR)/*.c) $(wildcard $(LIB_DIR)/**/*.c))

This line is responsible for generating the paths for all object files by:

  • Using wildcard to find all .c files in SRC_DIR and LIB_DIR.
  • Transforming each .c filename to its corresponding .o filename using patsubst.

Object files are intermediate files generated during the build process. They are used to store the compiled code of each source file, and they are linked together to create the final executable. The Makefile assumes that the directory structure inside the build directory mirrors that of root directory (only for files relevant to the build process).

Compiler Settings

makefile

CC := clang-18
LINTER := clang-tidy-18
FORMATTER := clang-format-18

Here, we set the compiler to clang-18 and define tools for linting (clang-tidy-18) and formatting (clang-format-18).

Compiler and Linker Flags Settings

makefile

CFLAGS := -std=gnu17 -D _GNU_SOURCE -D __STDC_WANT_LIB_EXT1__ -Wall -Wextra -pedantic
LDFLAGS := -lm

This section defines:

  • CFLAGS: Compilation flags, which specify:
    • The C standard to use (gnu17).
    • Definitions for enabling GNU and C11 extensions.
    • Various warning flags.
  • LDFLAGS: Linker flags, like linking to the math library (libm).

makefile

ifeq ($(debug), 1)
	CFLAGS := $(CFLAGS) -g -O0
else
	CFLAGS := $(CFLAGS) -Oz
endif

This conditional block adds debugging flags (-g -O0) to CFLAGS if debug is set to 1. Otherwise, it sets the optimization level to -Oz.

Targets

makefile

$(NAME): format lint dir $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN_DIR)/$@ $(patsubst %, build/%, $(OBJS))

This target is the one generating the final executable for gnaro. It also ensures that the source code is formatted and linted before building. The $(OBJS) variable is used to specify the object files to link together.

Object files are built using the following target:

makefile

$(OBJS): dir
	@mkdir -p $(BUILD_DIR)/$(@D)
	@$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c

Each object file depends on its corresponding source file. This target compiles each source file into its object file.

Run tests

makefile

test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test

This target compiles and runs the tests using the CUnit testing framework.

Linting, Formatting, and Memory Checking

The lint, format, and check targets handle code quality checks using the specified tools.

makefile

# Run CUnit tests
test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test
 
# Run linter on source directories
lint:
	@$(LINTER) --config-file=.clang-tidy $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/* -- $(CFLAGS)
 
# Run formatter on source directories
format:
	@$(FORMATTER) -style=file -i $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/*
 
# Run valgrind memory checker on executable
check: $(NAME)
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --help
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --version
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< -v

Setup Dependencies

makefile

setup:
	# ... OS and tool installation commands ...

I included a target for setting up the project and installing required OS packages and development tools on a new system. Initially, I used scripts to automate this process, but I found that it was cleaner to include it in the Makefile.

Directory Setup and Cleanup

The dir target ensures the build and bin directories exist, while the clean target removes them.

makefile

# Setup build and bin directories
dir:
	@mkdir -p $(BUILD_DIR) $(BIN_DIR)
 
# Clean build and bin directories
clean:
	@rm -rf $(BUILD_DIR) $(BIN_DIR)

Phony Targets

makefile

.PHONY: lint format check setup dir clean bear

.PHONY tells make that the listed targets don't correspond to actual files. This ensures they always execute, even if a file with the same name exists.

Bear

Bear is a tool that generates a compilation database for clang tooling. It is used to generate a compile_commands.json file, which is used by tools like clang-tidy and clang-format to determine how to handle the source files.

makefile

bear --exclude $(LIB_DIR) make $(NAME)

Full Makefile

I am including the full Makefile below for reference:

makefile

# Project Settings
debug ?= 0
NAME := gnaro
SRC_DIR := src
BUILD_DIR := build
INCLUDE_DIR := include
LIB_DIR := lib
TESTS_DIR := tests
BIN_DIR := bin
 
# Generate paths for all object files
OBJS := $(patsubst %.c,%.o, $(wildcard $(SRC_DIR)/*.c) $(wildcard $(LIB_DIR)/**/*.c))
 
# Compiler settings
CC := clang-18
LINTER := clang-tidy-18
FORMATTER := clang-format-18
 
# Compiler and Linker flags Settings:
# 	-std=gnu17: Use the GNU17 standard
# 	-D _GNU_SOURCE: Use GNU extensions
# 	-D __STDC_WANT_LIB_EXT1__: Use C11 extensions
# 	-Wall: Enable all warnings
# 	-Wextra: Enable extra warnings
# 	-pedantic: Enable pedantic warnings
# 	-lm: Link to libm
CFLAGS := -std=gnu17 -D _GNU_SOURCE -D __STDC_WANT_LIB_EXT1__ -Wall -Wextra -pedantic
LDFLAGS := -lm
 
ifeq ($(debug), 1)
	CFLAGS := $(CFLAGS) -g -O0
else
	CFLAGS := $(CFLAGS) -Oz
endif
 
# Targets
 
# Build executable
$(NAME): format lint dir $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN_DIR)/$@ $(patsubst %, build/%, $(OBJS))
 
# Build object files and third-party libraries
$(OBJS): dir
	@mkdir -p $(BUILD_DIR)/$(@D)
	@$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c
 
# Run CUnit tests
test: dir
	@$(CC) $(CFLAGS) -lcunit -o $(BIN_DIR)/$(NAME)_test $(TESTS_DIR)/*.c
	@$(BIN_DIR)/$(NAME)_test
 
# Run linter on source directories
lint:
	@$(LINTER) --config-file=.clang-tidy $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/* -- $(CFLAGS)
 
# Run formatter on source directories
format:
	@$(FORMATTER) -style=file -i $(SRC_DIR)/* $(INCLUDE_DIR)/* $(TESTS_DIR)/*
 
# Run valgrind memory checker on executable
check: $(NAME)
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --help
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< --version
	@sudo valgrind -s --leak-check=full --show-leak-kinds=all $(BIN_DIR)/$< -v
 
# Setup dependencies for build and development
setup:
	# Update apt and upgrade packages
	@sudo apt update
	@sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y
 
	# Install OS dependencies
	@sudo apt install -y bash libarchive-tools lsb-release wget software-properties-common gnupg
 
	# Install LLVM tools required for building the project
	@wget https://apt.llvm.org/llvm.sh
	@chmod +x llvm.sh
	@sudo ./llvm.sh 18
	@rm llvm.sh
 
	# Install Clang development tools
	@sudo apt install -y clang-tools-18 clang-format-18 clang-tidy-18 valgrind bear
 
	# Install CUnit testing framework
	@sudo apt install -y libcunit1 libcunit1-doc libcunit1-dev
 
	# Cleanup
	@sudo apt autoremove -y
 
# Setup build and bin directories
dir:
	@mkdir -p $(BUILD_DIR) $(BIN_DIR)
 
# Clean build and bin directories
clean:
	@rm -rf $(BUILD_DIR) $(BIN_DIR)
 
# Run bear to generate compile_commands.json
bear:
	bear --exclude $(LIB_DIR) make $(NAME)
 
.PHONY: lint format check setup dir clean bear

Limitations

While the gnaro project Makefile offers a structured and comprehensive build process, it does have some limitations. For instance, it's tailored specifically to environments with the apt package manager, making it less portable to non-Debian-based systems. The Makefile also assumes the availability of specific versions of tools like clang-18, which could pose challenges if the project is moved to environments with different tool versions or if these tools are deprecated in the future. Additionally, the heavy reliance on implicit rules and predefined directories might make the Makefile less flexible to changes in the project's structure or build requirements. However, these limitations are outweighed by the benefits of having a clean and maintainable Makefile.

Conclusion

Crafting a clean and maintainable Makefile doesn't have to be a complex ordeal. By leveraging Makefile features like variables, wildcards, automatic variables, and phony targets, and by commenting liberally, I was able to create a Makefile that is not only functional but also easy to understand and modify. This straightforward approach has streamlined the build process of my C project, demonstrating the utility and simplicity of Makefiles in managing build processes.