接上一篇, Makefile 浅析 (1) 只是简单的罗列了几个要点, 以及几个常用函数, 对于实战并没有很大的帮助. 那这篇呢, 是想借一个小项目, 来应用前面学到的知识点.
这里, 先了解下练手项目的基本要求:
1. 将所有的目标文件放在 objs 目录中
2. 将所有生成的可执行文件放在 exes 目录中
3. 将引入用户头文件来模拟复杂项目
1. 创建目录
.PHONY: all
MKDIR = mkdir
DIRS = objs exes
all: $(DIRS) # 如果没有这句, 则执行结果为 mkdir objs.
$(DIRS):
$(MKDIR) $@
2. 创建 clean 目标
.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIRS = objs exes
all: $(DIRS)
$(DIRS):
$(MKDIR) $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
3. 添加源文件
这里共有四个文件:
$ ls
foo.c foo.h main.c makefile
$ cat foo.c
#include <stdio.h>
#include "foo.h"
void foo()
{
printf (“This is foo ()!\n”);
}
$ cat foo.h
#ifndef __FOO_H
#define __FOO_H
void foo();
#endif
$ cat main.c
#include "foo.h"
int main ()
{
foo();
return 0;
}
对于加入的源程序, 在 makefile 中添加编译规则:
.PHONY: all clean
CC = gcc
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
EXE = my.exe
DIRS = objs exes
SRCS = $(wildcard ./*.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
all: $(DIRS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) -o $@ -c $^
clean:
$(RM) $(RMFLAGS) $(DIRS) $(EXE) $(OBJS)
4. 将目标文件放入指定目录
.PHONY: all clean
CC = gcc
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIR_OBJS = objs
DIR_EXES = exes
DIRS = $(DIR_OBJS) $(DIR_EXES)
EXE = my.exe
# 添加前缀, 以将目标文件放入指定目录
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
SRCS = $(wildcard ./*.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
# 添加前缀, 以将目标文件放入指定目录
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
all: $(DIRS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE): $(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o: %.c # 加路径的时候忘了 / , 找了好久
$(CC) -o $@ -c $^
clean:
$(RM) $(RMFLAGS) $(DIRS)
5. 深入复杂关系
上面的关系中, 并没有出现对 .h 文件的依赖. 也就是说, 如果在已编译过的情况下, 修改了 foo.h 文件, 重新 make 并不会报错, 因为 make 没有发现. 需要 clean 掉重新 make, 但这样效率太低了.
下面是项目很小的时候可行的办法, 直接加上头文件依赖:
$(DIR_OBJS)/%.o: %.c foo.h # 添加头文件依赖
$(CC) -o $@ -c $< # 使用自动变量, 取第一个
如果项目比较大, 头文件引用的比较多, 则使用 include 包含依赖文件会比较合适:
DEPS = deps
# 如果在 include 前加上一个‘-’号,当 make 处理这一包含指示时,如果文件不存在就会忽略这一错误
-include $(DEPS)
以下, 增加 DEPS 目录用于存放依赖文件; 并将所有的目录创建都放到相应的规则中, 并作为第一个先决条件(因为, make 对于 include 的处理是先于 all 目标的构建的,这样的话, 由于依赖文件是在构建 all 目标时才创建的,所以很自然 make 在处理 include 指令时,是找不到依赖文件的)
.PHONY: all clean
CC = gcc
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = my.exe
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
all: $(EXE)
-include $(DEPS)
$(DIRS):
$(MKDIR) $@
# 将所有的目录创建都放到相应的规则中, 并作为第一个先决条件
$(EXE): $(DIR_EXES) $(OBJS)
$(CC) -o $@ $(filter %.o, $^) # 去除目录
$(DIR_OBJS)/%.o: $(DIR_OBJS) %.c
$(CC) -o $@ -c $(filter %.c, $^)
# 这里会出现重复, 问题还未知, 可能 makefile 旧写法的问题吧? 还是构建依赖文件的动作有点多余?
$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
@echo "Making $@ ..."
@set -e; \
$(CC) -E -MM $(filter %.c, $^) | awk 'BEGIN{printf("'"$(DIR_OBJS)"'/")}{print $N}' > $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
这里我们又有一个知识点需要注意,对于规则中的每一个命令,make 都是在一个新的 Shell 上运行它的,如果希望多个命令在同一个 Shell 中运行,则需要用‘;’将这些命令连起来。当命令很长时,为了方便阅读,我们需要将一行命令分成多行,这需要用‘\’。
set -e 的作用是告诉 BASH Shell 当生成依赖文件的过程中出现任何错误时,就直接退出
gcc -E -MM 是列出目标的依赖关系, 然后保存到文件里, 再 include 包含进去.
在这里, 可以将依赖本身添加到依赖文件的目标中, 型如:
objs/foo.o deps/foo.dep: foo.c foo.h define.h
# 这里, deps/foo.dep 就是依赖关系
当依赖变化时, make 就能发现需要重新生成依赖文件, 以遍重新加入新的文件, 调整依赖关系, 否则, 若新添加一个头文件而未改变源码文件, 则 make 并不能发现.
.PHONY: all clean
CC = gcc
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = my.exe
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
all: $(EXE)
-include $(DEPS)
$(DIRS):
$(MKDIR) $@
$(EXE): $(DIR_EXES) $(OBJS)
$(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o: $(DIR_OBJS) %.c
$(CC) -o $@ -c $(filter %.c, $^)
# 在这里增加依赖关系
$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
@echo "Making $@ ..."
@set -e; \
$(CC) -E -MM $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]* ,$(DIR_OBJS)/\1.o $@: ,g' > $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
解决方案: 上面的案例, 并不是很完美, 原作者的 makefile 并不通用. 没有考虑到文件更新的同时, 该文件所在的目录也一并更新了, 这导致在生成目标文件或者依赖文件后, 再次 make 还处于修改状态, 所以会有重复操作.
.PHONY: all clean
CC = gcc
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIR_OBJS = objs
DIR_EXES = exes
DIR_DEPS = deps
# 依赖文件夹被更新而导致无限 include, 所以这里当已存在 deps 目录时取消它的依赖
ifeq ("$(wildcard $(DIR_DEPS))", "")
DIR_DEPS_PRE := $(DIR_DEPS)
endif
# 与上面的类似, objs 目录先创建, 然后将生成的文件放入目录导致 objs 目录时间更新
ifeq ("$(wildcard $(DIR_OBJS))", "")
DIR_OBJS_PRE := $(DIR_OBJS)
endif
DIRS = $(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE = my.exe
# 添加前缀, 以将目标文件放入指定目录
EXE := $(addprefix $(DIR_EXES)/, $(EXE))
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
# 添加前缀, 以将目标文件放入指定目录
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
all: $(EXE)
# 条件判断与括号之间要有空格分隔
ifneq ($(MAKECMDGOALS), clean)
-include $(DEPS)
endif
$(DIRS):
$(MKDIR) $@
# 将所有的目录创建都放到相应的规则中, 并作为第一个先决条件
$(EXE): $(DIR_EXES) $(OBJS)
$(CC) -o $@ $(filter %.o, $^) # 去除目录
$(DIR_OBJS)/%.o: $(DIR_OBJS_PRE) %.c # 这里的目录作为依赖, 经历创建此目录 -> 又放入目标文件, 一直都是较新状态, 所以在 make 时必须保证它为空 (不被依赖)
$(CC) -o $@ -c $(filter %.c, $^)
$(DIR_DEPS)/%.dep: $(DIR_DEPS_PRE) %.c # 这个目录依赖也是
@echo "Making $@ ..."
@set -e; \
$(CC) -E -MM $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]* ,$(DIR_OBJS)/\1.o $@: ,g' > $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
[1] 至简李云. 驾驭Makefile(准完整版) 2009.08.25
[2] colabean. Makefile中的include命令详解 2016-03-27
网友评论