如何用 Make 组织 C 库项目,并按需构建 examples 下的多个示例?
可以保留 examples/foo、examples/bar 这样的目录结构,用顶层 Makefile 统一管理库和示例。默认执行 make 只构建库,执行 make examples 构建全部示例,也可以直接指定某个示例的输出目标。
现有 Makefile 中的 $(CC) ... -o bin/libproject.a 执行的是链接操作。输出文件名以 .a 结尾,也不会自动生成静态库。静态库需要使用 ar 将目标文件归档。
先把库实现与示例入口分开
顶层 src/ 只放库的实现文件,用于演示的 main.c 移到相应示例的 src/ 中。每个示例都有自己的程序入口,分别链接成可执行文件。
对于这种规模的项目,可以先使用一个顶层 Makefile,暂时不必在各个示例目录中创建 Makefile:
project/
├── Makefile
├── include/
├── src/
├── examples/
│ ├── foo/
│ │ ├── include/
│ │ └── src/
│ │ └── main.c
│ └── bar/
│ ├── include/
│ └── src/
│ └── main.c
├── obj/ # 构建时生成
└── bin/ # 构建时生成
├── libproject.a
└── examples/
├── foo
└── bar
示例自己的头文件放在对应的 include/ 中;没有私有头文件时,可以省略该目录。
用顶层 Makefile 管理依赖
下面的写法使用 GNU make,适用于 GCC、Clang 等支持 -MMD -MP 的编译器。它假设库源码直接位于 src/*.c,各示例的源码直接位于 examples/<名称>/src/*.c,不会递归搜索更深层目录。
Makefile 中每条命令行必须以真正的 Tab 开头。
.DEFAULT_GOAL := all
SRC_DIR := src
OBJ_DIR := obj
BIN_DIR := bin
LIB := $(BIN_DIR)/libproject.a
LIB_SRC := $(wildcard $(SRC_DIR)/*.c)
LIB_OBJ := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/lib/%.o,$(LIB_SRC))
EXAMPLE_DIRS := $(patsubst %/,%,$(wildcard examples/*/))
EXAMPLE_NAMES := $(notdir $(EXAMPLE_DIRS))
EXAMPLE_SRC := $(wildcard examples/*/src/*.c)
EXAMPLE_OBJ := $(patsubst examples/%.c,$(OBJ_DIR)/examples/%.o,$(EXAMPLE_SRC))
EXAMPLE_BINS := $(addprefix $(BIN_DIR)/examples/,$(EXAMPLE_NAMES))
CPPFLAGS += -Iinclude -MMD -MP
CFLAGS += -Wall
LDLIBS += -lm
.PHONY: all examples clean
all: $(LIB)
examples: $(EXAMPLE_BINS)
$(LIB): $(LIB_OBJ)
mkdir -p $(@D)
$(AR) rcs $@ $^
$(OBJ_DIR)/lib/%.o: $(SRC_DIR)/%.c
mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
$(OBJ_DIR)/examples/%.o: examples/%.c
mkdir -p $(@D)
$(CC) $(CPPFLAGS) -I$(dir $<)../include $(CFLAGS) -c $< -o $@
define EXAMPLE_template
$(BIN_DIR)/examples/$(1): $(patsubst examples/%.c,$(OBJ_DIR)/examples/%.o,$(wildcard examples/$(1)/src/*.c)) $(LIB)
mkdir -p $$(@D)
$$(CC) $$(CFLAGS) $$(LDFLAGS) $$^ $$(LDLIBS) -o $$@
endef
$(foreach name,$(EXAMPLE_NAMES),$(eval $(call EXAMPLE_template,$(name))))
clean:
$(RM) -r $(BIN_DIR) $(OBJ_DIR)
-include $(LIB_OBJ:.o=.d) $(EXAMPLE_OBJ:.o=.d)
这里保留了原配置中的 -lm。如果库和示例都不依赖数学库,可以删除 LDLIBS += -lm。
EXAMPLE_template 为每个示例生成独立的链接规则。例如,foo 的目标依赖 examples/foo/src/ 下源码对应的目标文件,以及 bin/libproject.a。模板中的 $$ 让变量和自动变量经过 eval 后仍能在后续阶段展开。
-I$(dir $<)../include 根据当前源文件确定示例的私有头文件目录。例如,编译 examples/foo/src/main.c 时,对应路径是 examples/foo/src/../include。
如何构建
# 只构建静态库
make
# 构建全部示例,自动先构建所需的库
make examples
# 只构建 foo 及其依赖
make bin/examples/foo
# 并行构建全部示例
make -j examples
# 清理生成文件
make clean
如果以后希望默认同时构建示例,可以把默认目标改为:
all: $(LIB) examples
每个示例目录都需要有可链接成一个程序的源码,通常包含一个 main。上面的目录发现规则会把 examples/ 下的每个直接子目录视为示例;如果其中还放了公共资源目录,可以改成显式列表:
EXAMPLE_NAMES := foo bar
为什么这样安排
库的 .c 文件先编译成 .o,再由 $(AR) rcs 生成静态库。示例则通过 $(CC) 将自己的目标文件与静态库链接成可执行文件。
示例的依赖列表先列目标文件,再列 $(LIB),因此链接命令中的 $^ 也保持这一顺序。对于按命令行顺序处理静态库的链接器,这有助于正确解析符号;库依赖的 -lm 放在后面。
-MMD -MP 生成 .d 依赖文件,末尾的 -include 将其读入,使头文件修改后能触发重新编译。示例目标直接依赖静态库,因此库更新后,相关示例也会重新链接。
不同示例中的同名文件不会互相覆盖。foo/src/main.c 和 bar/src/main.c 分别生成到 obj/examples/foo/src/main.o 和 obj/examples/bar/src/main.o。
是否需要每个目录都有 Makefile
源码目录不必各自配备 Makefile。统一管理规则可以让 Make 直接看到库、目标文件和示例之间的依赖,比较适合当前这类小型项目。
如果不同示例以后有明显不同的构建流程,也可以改成递归 Make。调用子 Make 时应使用 $(MAKE) -C ...,并明确安排先完成库构建、再开始示例构建的依赖关系。仅把库和示例并列写成同一目标的前置条件,不能保证它们在并行构建时按先后顺序执行。
*nix 项目的几个注意点
libproject.a符合常见静态库命名习惯。放在bin/中可以工作;也可以按项目约定放到lib/或build/lib/,相应调整 Makefile 即可。- 公共头文件放在顶层
include/,只供库内部使用的头文件可以留在src/。示例通过公共头文件使用库。 - 库归档阶段无需使用
LDFLAGS或LDLIBS。这些选项用于最终程序的链接;归档本身不会把libm等依赖打包进静态库。 clean中列出的目录应专门存放生成文件,避免把手工维护的文件放进去。- 如果删除了库源码,已有
.a中的旧成员不会因ar rcs自动消失。遇到这种情况,应删除旧归档后重建,或执行make clean再构建。 - 这份配置包含 GNU make 扩展,并依赖特定编译器选项。如果需要支持 BSD make 或其他工具链,应另行调整构建规则。
备注:内容仅供参考。
