makefile简述
当你写完一个算法,只有一个源代码文件的时候,你要编译一下看看结果,这十分简单。
    clang -o helloworld.run helloworld.c
    ./helloworld.run
但是当你的项目有几十个C文件,然后你要把你的项目编译出来跑跑看,那就坑爹了。
    clang -o module_a.o -c module_a.c
    clang -o module_b.o -c module_b.c
    clang -o module_c.o -c module_c.c
    ...
    clang -o project.run module_a.o module_b.o module_c.o ...
    ./project.run
你说你可以把上面这一堆命令写成bash,每次改完代码要编译的时候跑一下这个脚本就好了。但这样做又会有下面这些问题:
1. 如果模块之间相互依赖,用bash来描述和维护这样的依赖关系会很麻烦
2. 如果我只是修改了其中一个文件,跑一遍bash会把所有文件都编译一遍,浪费了时间
makefile就是做了类似bash的事情,同时解决了如下三个问题:
1. 简化编译整个项目的操作
2. 提供了相对简便地描述依赖的方案
3. 只编译改动过的文件
makefile本质上只是命令的组织,通过make能够理解的语法来描述编译的依赖。然而具体到命令的内容,则是根据编译器的不同而不同的,本文使用的是clang,如果你用gcc或者其他的编译器,那就需要对命令稍作修改。具体编译器使用命令的介绍不在本文范围内,不过这并不影响你学习如何写makefile。
makefile是如何简化编译操作的
在你的项目目录里面创建一个GNUmakefile或makefile或Makefile文件,在这个文件里面写好内容(嗯,这篇文章就是教你怎么写makefile的内容的)。然后调用make程序就好了。
make程序会先找GNUmakefile, 然后是makefile, 然后是Makefile。一般来说GNUmakefile不太用,除非你的make是GNU make,否则别家的make程序识别不出来。然后比较推荐的是使用Makefile,因为这样在终端里面ls的时候比较醒目,它比较贴近目录列表,而且靠近README。
你也可以make -f mymakefile或者make --file=mymakefile,2B青年一般都喜欢这么做。
    $  ls
    makefile    src
    $  make
    clang -I./src -I../../utils -c src/demo.c -o build/demo.o 
    clang -I./src -I../../utils -c ../../utils/ArrayUtils.c -o build/ArrayUtils.o
    clang -I./src -I../../utils -o demo.run  ./build/demo.o ./build/ArrayUtils.o
    $  ls
    build       makefile    src         demo.run
demo.run就是编译生成的可执行文件。大多数情况下,你make就够了。build是由makefile里面的命令指定生成的文件夹,用来存放编译过程的中间文件,如果你没有在makefile里面写相关创建文件夹的指令,build不会自动出来。src是一个代码文件夹,里面存放源代码文件。
继续阅读前,你要知道的基础知识
- 源代码先编译成中间文件,比如c文件编译成o文件
 - 链接中间文件形成可执行程序,比如一堆o文件连接成一个可执行文件
 - 源文件的相互调用是在链接的时候进行协调的,所以只管走第一步就好了
 
没了。
makefile是如何描述依赖的
编译时,模块之间会存在各种依赖,makefile也会指导编译器按照依赖顺序去编译文件。make程序会从makefile中的第一个标签开始解析,所以第一个标签会成为整个依赖树的根。
描述依赖规则的方法是这样的:
  标签: 依赖列表
      模块编译语句
注意,模块编译语句的句首一定要有一个tab。没有依赖的话,依赖列表就可以不写。
下面举个例子:
fruit程序由2个源代码文件组成,分别是apple.c pineapple.c, pineapple.c有调用apple.c中的函数, 那么makefile就可以像下面这么写:
    # makefile里面的注释就是加个#就好了,跟bash一样
    start: apple.o pineapple.o
        clang -o fruit apple.o pine.o
    apple.o: apple.c
        clang -c apple.c -o apple.o
    pineapple.o: pine.c
        clang -c pine.c -o pine.o
- make在解析这个makefile的时候,会先走到
start标签,因为它是第一个(而不是因为它叫start)。然后make看到apple.o是它的依赖,于是他会去找apple.o这个标签并执行下面的clang语句。 - pineapple.c调用apple.c里面的函数这一茬,在start标签的命令里面,链接器会检查后面所有的.o文件的符号进行链接的,这个不用我们操心。
 - start里面的依赖列表的顺序是无所谓的,谁先谁后都可以
 
其实到这里,你完全可以自己写makefile来编译你自己的项目了,想知道更高级的技巧可以看后面。
makefile中的一些技巧
    INCLUDE_PATH = -I./src -I../../utils
    BUILD_DIR = ./build
    ALL_OBJ_O = $(BUILD_DIR)/demo.o $(BUILD_DIR)/ArrayUtils.o
    TARGET = demo.run
    CC = clang $(INCLUDE_PATH)
    start: prepare $(TARGET)
    prepare:
        mkdir -p $(BUILD_DIR)
    $(TARGET): $(ALL_OBJ_O)
        $(CC) -o $@  $(ALL_OBJ_O)
    $(BUILD_DIR)/demo.o: ./src/demo.c
        $(CC) -c $< -o $@
    $(BUILD_DIR)/ArrayUtils.o: ../../utils/ArrayUtils.c ../../utils/ArrayUtils.h
        $(CC) -c $< -o $@
    clean:
        rm -rf $(BUILD_DIR)
        rm -rf ./$(TARGET)
- 这里我定义了
INCLUDE_PATH、BUILD_DIR、ALL_OBJ_O、TARGET、CC这些变量,方便将来的扩展和改变 - 所有的依赖名其实都是基于字符串的,在start里面看到有prepare这个依赖,它并不是某个o文件。而且你也发现有些标签还带了路径,这些都没关系,make会找到对应字符串的标签,然后执行它
 - 编译语句里面写的其实还是bash命令,我把一些准备工作放到prepare标签下了
 - $<代表当前的依赖列表的第一项, 这里的标签正好就是要编译的c文件
 - $@代表标签名,这里的标签正好就是要输出的文件
 - clean标签在执行make的时候是不会走到的,但是执行make clean的时候就会走到了。聪明的你一定会发现,make后面跟什么标签,就会执行什么标签下的命令。你执行make prepare,那么就会执行prepare下的命令
 $(BUILD_DIR)/ArrayUtils.o: ../../utils/ArrayUtils.c ../../utils/ArrayUtils.h这里的h文件可以写也可以不写,写上的话,如果你修改了h文件,那么也会引起make重新链接target。如果不写,h文件即使有修改make也不管
makefile中的一些高级技巧
你可以省略将C文件编译成O文件的命令, make会自动根据标签名生成这样的命令:cc -c -o 标签.o 标签.c. 所以要是这么做,你就要指定一下编译器,export一下cc这个变量
    export cc=clang
    start: apple.o pineapple.o
        clang -o fruit apple.o pine.o
    apple.o:
    pineapple.o:
    # 当然你也可以写成这样:
    # apple.o: apple.h
    # pineapple.o: pineapple.h
    # 这样当h文件修改时,make就会重新链接了
如果你的某个标签可能跟某个文件重名,就可以使用.PHONY来标识这个标签, 详情参见Phony Targets
    start: apple.o pineapple.o clean
        clang -o fruit apple.o pine.o clean
    apple.o:
    pineapple.o:
    clean:
        clang -o clean -c clean.c
    .PHONY: clean
    clean:
        rm -rf ./build/.*
    # 如果不用PHONY,那么在编译fruit的时候,make遇到clean就不知道要走哪个标签了。
可以使用include指令来包含其他的makefile,描述文件名时可以用shell类似的拓展方法
    bar = bish bash
    include foo *.mk $(bar)
    # 等价于
    # include foo a.mk b.mk c.mk bish bash
总结
gnu上关于make的描述很长很详细,不过其实只要理解了makefile是用来组织各种命令的,那剩下的问题其实就都是如何把makefile写得更好。
我们也见到很多工程会先跑一个configure脚本,然后生成makefile,大多数情况下,这是使用autoconf这个程序来生成configure脚本,然后进而生成makefile的。
我们也见过有通过automake来编译部署程序的。这些已经是后期发布时要操作的事情了,开发的时候写个简单的makefile其实够用。这两个坑我先挖在这儿,以后有空再填。
    
Comments
comments powered by Disqus