Makefile简单入门

作者: MachinePlay | 来源:发表于2020-02-10 04:17 被阅读0次

    最近工作编译程序一直在用别人写的Makefile,但是没有系统的学习过,趁着放假学一波


    makefile

    0x00 Makefile 概述

    一个企业级项目,通常会有很多源文件,有时也会按功能、类型、模块分门别类的放在不同的目录中,有时候也会在一个目录里存放了多个程序的源代码。

    这时,如何对这些代码的编译就成了个问题。Makefile 就是为这个问题而生的,它定义了一套规则,决定了哪些文件要先编译,哪些文件后编译,哪些文件要重新编译。

    整个工程通常只要一个 make 命令就可以完成编译、链接,甚至更复杂的功能。可以说,任何一个 Linux 源程序都带有一个Makefile 文件。

    0x01 Makefile 的优点

    管理代码的编译,决定该编译什么文件,编译顺序,以及是否需要重新编译;节省编译时间。如果文件有更改,只需重新编译此文件即可,无需重新编译整个工程;一劳永逸。Makefile 通常只需编写一次,后期就不用过多更改。

    0x02 编译知识

    Makefile最初是为了编译C/C++而诞生的, 所以它里面的很多隐藏规则都是针对 C/C++的。在讲 Makefile 之前有必要对 C/C++的编译有一点了解

    过程如下: compile

    预处理器:将.c 文件转化成 .i 文件,使用的 gcc 命令是:gcc –E,对应于预处理命令 cpp;
    编译器:将.c/.h 文件转换成.s 文件,使用的 gcc 命令是:gcc –S,对应于编译命令 cc –S;
    汇编器:将.s 文件转化成 .o 文件,使用的 gcc 命令是:gcc –c,对应于汇编命令是 as;
    链接器:将.o 文件转化成可执行程序,使用的 gcc 命令是: gcc,对应于链接命令是 ld;
    加载器:将可执行程序加载到内存并进行执行,loader 和 ld-linux.so。

    0x03 Makefile规则

    Target...:   Prerequsites...
    Command
    Command
    ...
    

    Targets: Prerequisites;Command
    Command
    ...
    

    下面会称 Target 为目标, Prerequisites 为目标依赖, Command 为规则的命令行
    Command 必须以[Tab]开始, Command 可以写成多行,通过来继行,但行尾的后不能有空格。
    规则包含了文件之间的依赖关系和更新此规则 target 所需要的 Command

    targets 可以使用通配符, 如果格式是"A(M)"表示档案文件(.a)中的成员“

    在需要用本义的时候,使用两个$$来表示。
    当规则的 target 是一个文件,它的任何一个依赖文件被修改后,在执行 make <target>时这个目标文件都会被重新编译或重新连接。如果有必要此 target 的一个依赖文件也会被先重新编译。

    0x04伪目标

    Makefile 中把那些没有任何依赖只有执行动作的目标称为“伪目标“(Phony targets)

    .PHONY : clean
    clean :
    -rm edit $(objects
    

    通过.PHONY 将 clean 声明为伪目标,避免当目录下有名为“clean”文件时,clean 无法执行
    这样的目标不是为了创建或更新程序,而是执行相应动作。

    0x05自动推导规则

    在使用 make 编译.c 源文件时,编译.c 源文件规则的命令可以不用明确给出。这是因为 make 本身存在一个默认的规则,能够自动完成对.c 文件的编译并生成对应的.o 文件。它执行命令“cc -c”来编译.c 源文件。在 Makefile 中我们只需要给出需要重建的目标文件名(一个.o 文件),make 会自动为这个.o 文件寻找合适的依赖文件(对应的.c 文件。对应是指:文件名除后缀外,其余都相同的两个文件),而且使用正确的命令来重建这个目标文件。
    例如, 现在有三个文件 test.cpp, my.cpp, my.h

    image.png
    • test.cpp
    #include <iostream>
    #include "my.h"
    
    int main(int argc, char * argv[]) {
        int a = 100, b = 101;
        std::cout << "this code is for test makefile" << std::endl;
        std::cout << xadd(a, b) << std::endl;
    }
    
    • my.h
    #ifndef _MY_H_
    #define _MY_H_
    int xadd(const int x, const int y);
    #endif
    
    • my.cpp
    #include "my.h"
    int xadd(const int x, const int y)
    {
        return x + y;
    }
    

    对于上边的例子,此默认规则就使用命令“gcc -c test.cpp -o test.o”来创建文件“main.o”。对一个目标文件是“N.o”,倚赖文件是“N.c”的规则,完全可以省略其规则的命令行,而由 make 自身决定使用默认命令。此默认规则称为 make 的隐含规则。

    test: test.cpp my.o
        gcc  -c -o test test.cpp
    my.o: my.cpp my.h
        gcc -c  -o my.o  my.cpp
    
    clean :
        rm test  my.o
    

    也可以用隐式规则

    test: test.cpp my.o
    
    my.o: my.cpp my.h
    
    clean :
        rm test  my.o
    
    

    效果是一样的

    这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一 样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。 要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一 个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

    0x06 规则书写建议

    书写规则建议的方式是:单目标,多依赖。就是说尽量要做到一个规则中只存在一个目标文件,可有多个依赖文件。尽量避免使用多目标,单依赖的方式。

    0x07 makefile 工作原理文和件搜索顺序

    在默认的方式下,也就是我们只输入 make 命令。那么,

    1. 首先会搜索目录下的GNUmakefile,makefile,Makefile文件,或者make -f从指定文件读取
      2.找到makefile后首先从第一个target开始,如果生成target依赖别的目标就递归从依赖开始
      例如:上面的例子中,首先准备编译生成目标test,发现依赖my.o没有生成,就向下找my.o的生成,发现my.o的资源my.cpp,my.h已经就绪了,就先编译出my.o,回到test,发现 test.cppmy.o全部就绪,使用规则Command生成目标test

    这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在 找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所 定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系 之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

    通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命 令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean ,以此来清除所有 的目标文件,以便重编译。

    0x08 makefile中使用变量

    我们可以看到 .o 文件的字符串被重复了两次,如果我们的工程需要加入一个新的 .o 文件, 那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。
    当然,我们的makefile并不复 杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方, 而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也 就是一个字符串,理解成C语言中的宏可能会更好。

    比如,我们声明一个变量 obj,表示所有obj文件,在makefile的一开始就定义

    obj = my.o
    maincpp = test.cpp
    

    于是,我们就可以很方便地在我们的makefile中以 $(obj) 的方式来使用这个变量了,于是 我们的改良版makefile就变成下面这个样子:

    obj = my.o
    maincpp = test.cpp
    
    test :$(maincpp) $(obj)
    
    my.o: my.cpp my.h
    
    clean:
        rm $(obj)
    

    于是如果有新的 .o 文件加入,我们只需简单地修改一下 obj 变量就可以了。

    关于变量更多的话题,我会在后续给你一一道来。

    0x09 另类风格的makefiles

    既然我们的make可以自动推导命令,那么我看到那堆.o.h 的依赖就有点不爽,那么多的重复的 .h ,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动 推导命令和文件的功能呢?来看看最新风格的makefile吧。

    obj = my.o
    maincpp = test.cpp
    
    test :$(maincpp) $(obj)
    
    $(obj): my.h
    
    clean:
        rm $(obj) test
    

    这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。 还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个 新的.o 文件,那就理不清楚了。

    0x10 清空目标文件的规则

    每个Makefile中都应该写一个清空目标文件( .o 和执行文件)的规则,这不仅便于重编译,也很 利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是:

    clean:
      rm test $(obj)
    

    更为稳健的做法是:

    .PHONY: clean
    clean:
      rm  test $(obj)
    

    0x11 Makefile的文件名

    默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、 “makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile” 这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”, 这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说, 大多数的make都支持“makefile”和“Makefile”这两种默认文件名。

    当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris” ,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的-f--file参数

    make -f  Makefile.Linux
    make -f Makefile.mac
    

    0x12 引用其他的Makefile

    在Makefile使用include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

    include <filename>
    

    filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

    include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include<filename> 可以用一个或多个空格隔开。举个例子,你有这样几个Makefilea.mkb.mkc.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含 了 e.mkf.mk ,那么,下面的语句:

    include foo.make *.mk $(bar)
    

    等价于

    include foo.make a.mk b.mk c.mk e.mk f.mk
    
    1. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目 录下去寻找。

    2.如果目录 <prefix>/include (一般是: /usr/local/bin/usr/include )存在的话,make也会去找。

    如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的 文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是 不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以 在include前加一个减号“-”。如:

    -include <filename>
    

    0x13 环境变量MAKEFILES

    如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不 同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现 错误,make也会不理。

    但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时, 所有的Makefile都会受到它的影响,这绝不是你想看到的。在这里提这个事,只是为了告诉大家,也许 有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。

    0x14 变量定义及赋值:

    变量直接采用赋值的方法即可完成定义,如:

    INCLUDE = ./include/

    变量取值:

          用括号括起来再加个美元符,如:
    
          `FOO = $(OBJ)`
    

    系统自带变量:

    通常都是大写,比如 CCPWDCFLAG,等等。

    有些有默认值,有些没有。比如常见的几个:

    CPPFLAGS : 预处理器需要的选项 如:-I

    CFLAGS:编译的时候使用的参数 –Wall –g -c

    LDFLAGS :链接库使用的选项 –L -l

    变量的默认值可以修改,比如 CC 默认值是 cc,但可以修改为 gcc:CC=gcc

    0x15 函数

    Makefile 也为我们提供了大量的函数,同样经常使用到的函数为以下两个。需要注意的是,Makefile 中所有的函数必须都有返回值。在以下的例子中,假如目录下有 main.c、func1.c、func2.c 三个文件。

    通配符:

    用于查找指定目录下指定类型的文件,跟的参数就是目录+文件类型,比如:

    src = $(wildcard ./src/*.c)

    这句话表示:找到 ./src 目录下所有后缀为 .c 的文件,并赋给变量 src。

    命令执行完成后,src 的值为:main.c func1.c fun2.c。

    patsubst:

    匹配替换,例如以下例子,用于从 src 目录中找到所有 .c 结尾的文件,并将其替换为 .o 文件,并赋值给 obj。

    obj = $(patsubst %.c ,%.o ,$(src))

    命令执行完成后,obj 的值为 main.o func1.o func2.o。
    特别地,如果要把所有 .o 文件放在 obj 目录下,可用以下方法:

    obj = $(patsubst ./src/%.c, ./obj/%.o, $(src))

    更多可以参考https://seisman.github.io/how-to-write-makefile/overview.html

    相关文章

      网友评论

        本文标题:Makefile简单入门

        本文链接:https://www.haomeiwen.com/subject/hnhexhtx.html