示例项目结构
~/project$ tree
.
├── include
│ └── foo.h
├── lib
│ └── foo.c
├── Makefile
└── src
├── Makefile
├── test1.cc
├── test2.cc
└── test3.cc
3 directories, 7 files
lib/foo.c
是C编写的库文件,为foo()函数的具体实现,include/foo.h
是对应的头文件,包含foo()函数的声明,src
目录下的3个文件则是main()函数中调用foo()函数的源文件。
需求及相应gcc编译命令
现在需求是,把foo.c生成的.o文件放到目录obj
下(若不存在则新建),然后分别对src
目录下的文件进行编译生成各自的可执行文件,把生成的可执行文件放到目录bin
下(若不存在则新建)。
~/project$ mkdir -p build
~/project$ gcc -c lib/foo.c -o ./build/foo.o
~/project$ mkdir -p bin
~/project$ g++ -Wall -O2 -I./include src/test1.cc obj/foo.o -o ./bin/test1
~/project$ g++ -Wall -O2 -I./include src/test2.cc obj/foo.o -o ./bin/test2
~/project$ g++ -Wall -O2 -I./include src/test3.cc obj/foo.o -o ./bin/test3
~/project$ tree
.
├── bin
│ ├── test1
│ ├── test2
│ └── test3
├── include
│ └── foo.h
├── lib
│ └── foo.c
├── Makefile
├── obj
│ └── foo.o
└── src
├── Makefile
├── test1.cc
├── test2.cc
└── test3.cc
6 directories, 12 files
mkdir
命令的-p
选项则是在目录已经不存在时不报错,把该目录当成已经创建的目录。
可以发现,编译testx.cc(x=1,2,3)
的命令基本一致,都要和foo.o
一起链接,都要把include
目录作为包含目录。因此这些命令重复度较大,可以利用Makefile的进行简化。
Makefile的编写
特殊变量
-
$@
目标文件 -
$^
所有的依赖文件 -
$<
第一个依赖文件
也就是说对于下面的Makefile命令
foo.o: foo.c foo.h
gcc -c foo.c -o foo.o
可以简化为
foo.o: foo.c foo.h
gcc -c $< -o $@
这几个特殊变量即Makefile批量编译的基础支持,可以把这样的语句理解成类似C函数的概念,那么第二行就是函数体内容,$<
代指第1个输入参数,$@
代指输出参数。现在的问题是如何取得需要调用的列表。
函数
具体用法可以参考陈皓先生的跟我一起写Makefile
利用函数可以取得源文件列表和目标文件列表
# ~project/Makefile
SOURCE = $(wildcard src/*.cc)
PROGS = $(patsubst %.cc, bin/%, $(notdir $(SOURCE)))
-
wildcard
函数表示可以解释输入参数src/*.cc
的通配符,就像ls
命令一样。因此在上面的Makefile中,SOURCE
即src
目录下的.cc
文件列表src/test1.cc src/test2.cc src/test3.cc
-
notdir
函数表示找出输入参数中的非目录部分,比如对src/test1.cc
会返回test1.cc
,因此$(notdir $(SOURCE))
即test1.cc test2.cc test3.cc
-
patsubst
即字符串处理,参数3是批量处理的列表,参数1是模式,参数2是替换字符串。如果列表中的元素满足模式,则用参数2来替换。这里的%
类似通配符,比如test1.cc
满足模式%.cc
,那么替换字符串的%
则表示test1
。
也就是说,上述2句Makefile代码在我这个项目结构中等价于
SOURCE = src/test1.cc src/test2.cc src/test3.cc
PROGS = bin/test1 bin/test2 bin/test3
由于是动态生成的,假如src
目录下多了文件test4.cc
,上述变量会分别增加src/test4.cc
和src/test4
。
生成目录
参考GNU make下创建目录的问题
之前作者的做法类似于跟我一起学Makefile时的做法(.PHONY文件),把目录作为依赖项,然后目录下新建一个隐藏文件来作为目录的依赖项。因为在目录下创建新文件会导致目录的时间属性被更新,所以要用目录下的隐藏文件的时间属性来代替目录的时间属性。
在Make 3.80后的版本中,可以用order-only的依赖来解决。比如
../obj/foo.o: foo.c | ../obj
注意依赖参数../obj
前面加了个|
来表示该目录是order-only类型,因此在该目录下新建文件不会导致foo.o
被重新生成。
最终Makefile文件
# src/Makefile
CC = g++
FLAGS = -Wall
BIN = ../bin
LIB = ../lib
SRC = ../src
INC = ../include
OBJ = ../obj
FOO_LIB = $(OBJ)/foo.o
SOURCE = $(wildcard $(SRC)/*.cc)
PROGS = $(patsubst %.cc, $(BIN)/%, $(notdir $(SOURCE)))
all: $(PROGS) $(FOO_LIB)
$(BIN)/%: $(SRC)/%.cc $(FOO_LIB) $(INC)/foo.h | $(BIN)
$(CC) $(FLAGS) $< $(FOO_LIB) -o $@ -I$(INC)
$(BIN):
mkdir -p $@
$(FOO_LIB): $(LIB)/foo.c | $(OBJ)
gcc -c $< -o $@
$(OBJ):
mkdir -p $(OBJ)
clean:
rm -f $(FOO_LIB)
rm -f $(PROGS)
# Makefile
DIRS = src
MAKE = make
all:
for i in $(DIRS); do \
(cd $$i && echo "making $$i" && $(MAKE)) || exit 1; \
done
clean:
for i in $(DIRS); do \
(cd $$i && echo "cleaning $$i" && $(MAKE) clean) || exit 1; \
done
分别是子目录和父目录的Makefile,这种做法是学习apue.3e的做法,即支持在子目录下单独make,也支持在根目录下批量调用子目录下的make。需要注意的一点是使用shell语法的for循环时,Makefile中要用$$i
而非$i
。
网友评论