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)