Makefile语法
基础概念
什么是 Makefile
Makefile 是一个工程文件的编译规则,它告诉 make 命令如何编译和链接程序。
基本格式
目标: 依赖1 依赖2 ...
命令1
命令2
...
注意:命令行前必须是 Tab,不能是空格
简单示例
hello: hello.c
gcc hello.c -o hello
工作原理
make 命令会在当前目录下寻找 Makefile 或 makefile 文件找到文件后,会寻找第一个目标文件(target)根据依赖关系,确定是否需要重新生成目标文件如果依赖文件比目标文件新,则执行命令重新生成目标
基本规则
目标文件:通常是要生成的文件名依赖文件:生成目标文件所需的文件命令:具体的执行命令,必须以 Tab 开头
伪目标
不代表实际文件的目标,通常用于执行命令
.PHONY: clean
clean:
rm -f *.o
通配符
*: 匹配任意字符串?: 匹配单个字符[...]: 匹配括号中的任意一个字符
示例:
*.o: *.c
gcc -c $<
常见的文件结构
# 编译器设置
CC = gcc
CFLAGS = -Wall
# 目标文件
TARGET = myprogram
# 源文件和对象文件
SRCS = main.c helper.c
OBJS = main.o helper.o
# 默认目标
all: $(TARGET)
# 生成可执行文件
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
# 生成目标文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET)
常见错误
空格vs制表符
命令行必须以制表符(Tab)开头,不能用空格常见错误信息:Makefile:4: *** missing separator. Stop.
循环依赖
A依赖B,B又依赖A错误信息:Circular xxx <- xxx dependency dropped.
命令执行错误
如果某条命令执行失败,make会停止执行使用 -k 参数可以继续执行其他命令
调试技巧
使用 make -n 显示要执行的命令但不实际执行使用 make -d 显示调试信息在变量前加 @ 可以不显示该命令的执行过程
变量
基本变量定义
# 定义变量
SOURCE_FILES = main.c utils.c
OBJECTS = $(SOURCE_FILES:.c=.o)
预定义变量
$@: 当前目标$<: 第一个依赖文件$^: 所有依赖文件$*: 匹配符 % 匹配的部分$(@D): 目标文件的目录部分$(@F): 目标文件的文件部分
环境变量
可以使用 shell 环境变量,如 $(HOME), $(PATH)使用 export 可以将 Makefile 变量导出到环境中
隐式变量
Make 预定义的变量:
CC # C 编译器,默认 cc
CXX # C++ 编译器,默认 g++
CFLAGS # C 编译选项
CXXFLAGS # C++ 编译选项
LDFLAGS # 链接器选项
AR # 静态库打包工具,默认 ar
ARFLAGS # ar 命令选项
RM # 删除命令,默认 rm -f
赋值
赋值运算符
= 普通赋值
A = hello
B = $(A) world # B 的值会随 A 的变化而变化
:= 立即赋值
A := hello
B := $(A) world # B 的值在此时固定
?= 条件赋值
A ?= hello # 只有 A 未定义时才赋值
+= 追加赋值
A = hello
A += world # A 变成 "hello world"
函数
字符串函数
# 替换字符串
$(subst from,to,text)
# 模式替换
$(patsubst pattern,replacement,text)
# 去除空格
$(strip string)
# 查找字符串
$(findstring find,text)
文件名函数
# 获取目录名
$(dir src/foo.c hacks) # 返回 src/ ./
# 获取文件名
$(notdir src/foo.c hacks) # 返回 foo.c hacks
# 添加后缀
$(suffix src/foo.c src/bar.h) # 返回 .c .h
# 替换后缀
$(basename src/foo.c src/bar.h) # 返回 src/foo src/bar
其他常用函数
# 获取 shell 命令输出
FILES := $(shell ls *.c)
# 循环函数
$(foreach var,list,text)
# 条件判断
$(if condition,then-part,else-part)
# 调用其他函数
$(call variable,param1,param2)
条件判断
if/else 语句
ifeq ($(CC),gcc)
CFLAGS += -Wall
else
CFLAGS += -W
endif
ifdef DEBUG
CFLAGS += -g
endif
ifndef RELEASE
CFLAGS += -O0
endif
条件函数
# 检查变量是否相等
result = $(if $(filter $(CC),gcc),YES,NO)
# 检查文件是否存在
exists = $(if $(wildcard $(file)),YES,NO)
参数
命令行参数
make -j4 # 并行执行,最多4个作业
make V=1 # 显示详细编译信息
make DEBUG=1 # 启用调试模式
常用参数选项
-f file: 指定 makefile 文件名-C dir: 切换到指定目录-n: 显示要执行的命令但不执行-s: 静默模式-k: 出错后继续执行-j N: 并行执行 N 个作业-B: 强制重新构建所有目标--debug: 输出调试信息
实际示例
# 完整的 Makefile 示例
CC := gcc
CFLAGS := -Wall -O2
TARGET := myapp
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
# 调试模式
ifdef DEBUG
CFLAGS += -g -DDEBUG
endif
# 默认目标
all: $(TARGET)
# 链接
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@
# 编译
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
.PHONY: clean
clean:
$(RM) $(OBJS) $(TARGET)
常用模式和最佳实践
多目录项目结构
# 目录结构
SRC_DIR := src
OBJ_DIR := obj
INC_DIR := include
BIN_DIR := bin
# 源文件和目标文件
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
# 确保目录存在
$(shell mkdir -p $(OBJ_DIR) $(BIN_DIR))
# 编译规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@
自动依赖生成
# 生成依赖文件
DEPS := $(OBJS:.o=.d)
# 包含依赖文件
-include $(DEPS)
# 生成依赖的规则
$(OBJ_DIR)/%.d: $(SRC_DIR)/%.c
@set -e; rm -f $@; \
$(CC) -MM -I$(INC_DIR) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,$(OBJ_DIR)/\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
常用目标
.PHONY: all clean install uninstall dist help
# 默认目标
all: $(TARGET)
# 安装
install: $(TARGET)
install -d $(DESTDIR)/usr/local/bin
install -m 755 $(TARGET) $(DESTDIR)/usr/local/bin
# 卸载
uninstall:
rm -f $(DESTDIR)/usr/local/bin/$(TARGET)
# 打包发布
dist: clean
mkdir -p $(TARGET)-$(VERSION)
cp -R * $(TARGET)-$(VERSION)
tar czf $(TARGET)-$(VERSION).tar.gz $(TARGET)-$(VERSION)
rm -rf $(TARGET)-$(VERSION)
# 帮助信息
help:
@echo "make - 编译程序"
@echo "make clean - 清理编译文件"
@echo "make install - 安装程序"
@echo "make dist - 创建发布包"
调试和发布配置
# 配置选项
DEBUG ?= 0
RELEASE ?= 1
# 调试配置
ifeq ($(DEBUG), 1)
CFLAGS += -g -DDEBUG
LDFLAGS += -g
endif
# 发布配置
ifeq ($(RELEASE), 1)
CFLAGS += -O2 -DNDEBUG
LDFLAGS += -s
endif
跨平台支持
# 操作系统检测
ifeq ($(OS),Windows_NT)
PLATFORM := Windows
EXE := .exe
RM := del /Q
else
PLATFORM := $(shell uname -s)
EXE :=
RM := rm -f
endif
# 平台相关设置
ifeq ($(PLATFORM),Windows)
CFLAGS += -D_WIN32
else ifeq ($(PLATFORM),Linux)
CFLAGS += -D__linux__
else ifeq ($(PLATFORM),Darwin)
CFLAGS += -D__APPLE__
endif
库文件处理
# 静态库
STATIC_LIB := libexample.a
$(STATIC_LIB): $(OBJS)
$(AR) rcs $@ $^
# 动态库
SHARED_LIB := libexample.so
$(SHARED_LIB): $(OBJS)
$(CC) -shared -o $@ $^ $(LDFLAGS)
# 库的安装
install-lib: $(STATIC_LIB) $(SHARED_LIB)
install -d $(DESTDIR)/usr/local/lib
install -m 644 $(STATIC_LIB) $(DESTDIR)/usr/local/lib
install -m 755 $(SHARED_LIB) $(DESTDIR)/usr/local/lib
ldconfig
版本控制集成
# 获取 Git 版本信息
VERSION := $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse --short HEAD)
BUILD_TIME := $(shell date +"%Y-%m-%d %H:%M:%S")
# 编译时加入版本信息
CFLAGS += -DVERSION=\"$(VERSION)\" \
-DCOMMIT=\"$(COMMIT)\" \
-DBUILD_TIME=\"$(BUILD_TIME)\"
性能优化
# 并行编译
NPROCS := $(shell nproc)
MAKEFLAGS += -j$(NPROCS)
# 缓存支持
CCACHE := $(shell which ccache)
ifneq ($(CCACHE),)
CC := ccache $(CC)
endif
# 编译优化
CFLAGS += -pipe -march=native
测试集成
.PHONY: test check coverage
# 单元测试
TEST_DIR := tests
TEST_SRCS := $(wildcard $(TEST_DIR)/*.c)
TEST_BINS := $(TEST_SRCS:$(TEST_DIR)/%.c=$(TEST_DIR)/%)
test: $(TEST_BINS)
@for test in $(TEST_BINS); do ./$$test; done
# 代码覆盖率
coverage:
$(MAKE) clean
$(MAKE) CFLAGS="$(CFLAGS) --coverage"
$(MAKE) test
gcov $(SRCS)