Shell和命令

Makefile生成目录+多文件批量编译

2018-03-08  本文已影响21人  哈莉_奎茵

示例项目结构

~/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)))
  1. wildcard函数表示可以解释输入参数src/*.cc的通配符,就像ls命令一样。因此在上面的Makefile中,SOURCEsrc目录下的.cc文件列表src/test1.cc src/test2.cc src/test3.cc
  2. notdir函数表示找出输入参数中的非目录部分,比如对src/test1.cc会返回test1.cc,因此$(notdir $(SOURCE))test1.cc test2.cc test3.cc
  3. 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.ccsrc/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

上一篇 下一篇

猜你喜欢

热点阅读