为什么要学习makefile

在linux下开发项目,如果想要完成一个大型项目的开发,可能在windows环境下,有许多编译器就已经替代了makefile功能,但在linux下想要合理管理代码,学会编写makefile就非常重要了。

makefile 关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中, makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。

编写makefile文件本质上是帮组make如何一键编译,进行批处理,makefile文件包含的规则命令使我们不需要繁琐的操作,提高了开发效率。

其实,在makefile文件的编写,我们参照的是linux内核管理庞大代码的风格编写makefile。

makefile在内核中主要分为三大类:

  1. 总控makefile
  2. 功能目录makefile
  3. scripts下的makefile

    如此管理代码,自然有非常多的好处。一是提高了代码的维护性,二是间接的提高了代码的可读性。

    下面介绍一下makefile体系的工作原理
    在scripts目录下的makefile就类似c语言中的头文件,里面主要定义了变量。在总控makefile下一般有如下一行代码:

include scripts/Makefile

当我们启用make命令时,首先寻找总控makefile并执行makefile文件,在执行上述代码时,将定义的变量在总控makefile中展开,然后在进入各个功能目录下makefile执行,最总生成我们最终需要的目标。
下面我以一个简单的实例想大家展示makefile的编写。代码在全文最后贴出。主要实现一个加减乘除的计算器功能。

首先先创建以下几个功能目录

mkdir -p main/src sub/src add/src mul/src div/src//用这条命令建立各个功能目录

mkdir scripts//建立这个目录存放变量的Makefile

touch Makefile //建立总控makefile

touch main/src/Makefile
touch ....
//分别在各个功能目录src下建立Makefile目录。

见图

tree//将makefile以如下显示出来

这里写图片描述

这样就可以清楚的管理代码,间接提高开源文件代码的阅读性。

如何填写makefile的内容

下面讲一下makefile的编写,其实就是makefile的规则。
target … : prerequisites …
command


target 也就是一个目标文件,可以是 Object File,也可以是执行文件。还可以是一个标签( Label)。
prerequisites 就是,要生成那个 target 所需要的文件或是目标。command 也就是 make 需要执行的命令。(任意的 Shell 命令)
这是一个文件的依赖关系,也就是说, target 这一个或多个的目标文件依赖于 prerequisites中的文件,其生成规则定义在 command 中。说白一点就是说, prerequisites 中如果有一个以上的文件比 target 文件要新的话, command 所定义的命令就会被执行。这就是 Makefile的规则。也就是 Makefile 中最核心的内容。

目标、依赖、命令就是makefile规则编写的三要素。

下面举一个简单的例子(摘自网络)

如果一个工程有 3 个头文件,和 8 个 C 文件,我们为了完成前面所述的那三个规则,我们的 Makefile 应该是下面的这个样子的。

edit : main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o4
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o

反斜杠( /)是换行符的意思。这样比较便于 Makefile 的易读。我们可以把这个内容保存在文件为“ Makefile”或“ makefile”的文件中,然后在该目录下直接输入命令“ make”就可以生成执行文件 edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行
一下“ make clean”就可以了。
在这个 makefile 中,目标文件( target)包含:执行文件 edit 和中间目标文件( *.o),依赖文件( prerequisites)就是冒号后面的那些 .c 文件和 .h 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。

记住, make 并不管命令是怎么工作的,他只管执行所定义的命令。 make会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比targets 文件的日期要新,或者 target 不存在的话,那么, make 就会执行后续定义的命令。这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像 C 语言中的lable 一样,其冒号后什么也没有,那么, make 就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个 lable的名字。这样的方法非常有用,我们可以在一个 makefile 中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

make如何工作

在默认的方式下,也就是我们只输入 make 命令。那么,
1、 make 会在当前目录下找名字叫“ Makefile”或“ makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件( target),在上面的例子中,他会找到“ edit”
这个文件,并把这个文件作为最终的目标文件。
3、如果 edit 文件不存在,或是 edit 所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
4、如果 edit 所依赖的.o 文件也不存在,那么 make 会在当前文件中找目标为.o 文件的依赖性,如果找到则再根据那一个规则生成.o 文件。(这有点像一个堆栈的过程)
5、当然,你的 C 文件和 H 文件是存在的啦,于是 make 会生成 .o 文件,然后再用 .o 文件生成make 的终极任务,也就是执行文件 edit 了。

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

通过上述分析,我们知道,像 clean 这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要 make 执行。即命令—— “ makeclean”,以此来清除所有的目标文件,以便重编译。于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如 file.c,那么根据我们的依赖性,我们的目标 file.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 file.o 的文件也是最新的啦,于是 file.o 的文件修改时间要比 edit 要新,所以edit 也会被重新链接了(详见 edit 目标文件后定义的命令)。而如果我们改变了“ command.h”,那么, kdb.o、command.o 和 files.o 都会被重编译,并且,edit 会被重链接。

如何在scripts下的Makefile定义变变量

变量的定义说明

变量在声明时需要给予初值,而在使用时,需要在变量名前加上“$”符号,但最好用小括号“()”或是花括号”{}”把变量给引用起来。

下面Makefile等同:
objects=program.o foo.o utils.o
program: (objects)ccoprogram (objects)
$(objects):defs.h
等同于:
objects=program.o foo.o utils.o
program:program.o foo.o utils.o
cc -o program program.o foo.o utils.o
program.o foo.o utils.o:defs.h

赋值变量
1、使用“=”操作符,使得前面的变量可以通过后面的变量来定义。
1、使用“:=”操作符,使得前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。
2、使用”?=”操作符:例如foo ?= bar 如果foo没有被定义过,那么变量foo的值就是“bar”,否则此语句什么也不做。
3、使用“+=”操作符,可以追加值
objects=main.o foo.o bar.o
objects+=another.o
等同于
objects=main.o foo.o bar.o
objects:=$(objects) another.o

下面给出之前给出的计算器的makefile的写法

总控makefile

include scripts/Makefile

modules_make = $(MAKE) -C $(1);
modules_clean = $(MAKE) clean -C $(1);

.PHONY: all mm mc clean


all : $(Target)

mm:
    @ $(foreach n,$(Modules),$(call modules_make,$(n)))
mc:
    @ $(foreach n,$(Modules),$(call modules_clean,$(n)))

$(Target) : mm
    @$(CC) -o  $(Target) $(Allobjs)  -lsqlite3 -lpthread

clean : mc
    @rm -rf $(Target)

scripts下makefile

CC := gcc -lpthread -lsqlite3
Target := cal
Source := $(wildcard src/*.c)
Objs := $(patsubst %.c,%.o,$(Source))
Modules += main sub div mul add
Allobjs := $(addsuffix /src/*.o,$(Modules))

各个功能目录下makefile类似。

include ../scripts/Makefile

all:    $(Objs)

clean:
    rm -rf $(Objs)

下面对上述makefile做的一个补充

嵌套执行

subsystem:
            cd subdir && $(MAKE)

其等价于:

subsystem:
        $(MAKE) -C subdir

这句是Makefile的嵌套执行:这里的$(MAKE)就相当于make,-C 选项的作用是指将当前工作目录转移到你所指定的位置。

一些make函数的补充

函数补充

上面代码的分析,以后再做补充。
以下为源代码

main.c文件

#include <stdio.h>

int main()
{
    printf("add = %d\n",add(6,3));
    printf("sub = %d\n",sub(6,3));
    printf("mul = %d\n",mul(6,3));
    printf("div = %d\n",div(6,3));

    return 0;
}

add.c

int add(int a,int b)
{
    return a+b;
}

sub.c

int sub(int a,in b)
{
     return a - b;
}

mul.c

int mul(int a,int b)
{  
   return a * b;
}

div.c

int div(int a,int b)
{ 
   return a/b;
}

今日总结:
makefile的编写最近是个难点,希望不要惧怕,把每个细节都要搞懂,毕竟makefile的编写不是一蹴而就的。

Logo

更多推荐