本帖最后由 东吴 于 2024-6-1 15:19 编辑
一、Makefile介绍 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,Makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行更复杂的功能操作。 1、基本组成 Makefile 里主要包含了五个东西:显示规则、隐晦规则、变量定义、文件指示和注释。 (1)显示规则。显示规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。 (2)隐晦规则。由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 所支持的。 (3)变量的定义。在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像 C 语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。 (4)文件指示。其包括了三个部分,一个是在一个 Makefile 中引用另一个Makefile,就像 C 语言中的 include 一样;另一个是指根据某些情况指定Makefile 中的有效部分,就像 C 语言中的预编译#if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。 (5)注释。Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符, 这个就像 C/C++中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“\#”。 最后,还值得一提的是,在 Makefile 中的命令,必须要以[Tab]键开始。
2、Makefile三要素 语法: [目标]:[依赖] (tab制表符)[命令] 目标:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签.一般来说,我们的目标基本上是一个文件,但也有可能是多个文件;
依赖:是是没有.如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生我们的依赖文件,要生成 targets 需要的文件或者是另一个目标。可以是多个,也可以成的; 命令:make 需要执行的命令(任意的 shell 命令)。如果其不与“目标,依赖”在一行,那么,必须以[Tab键]开头,如果和“目标,依赖”在一行,那么可以用分号做为分隔。 如下 [目标]:[依赖];[命令] (tab制表符)[命令]
如果命令太长,你可以使用反斜框(‘\’)作为换行符。make 对一行上有多少个字符没有限制。规则告诉 make 两件事,文件的依赖关系和如何成成目标文件。
3、Makefile工作原理 输入make命令:会执行Makefile的第一个目标,如果该目标有依赖,那么也会自动执行依赖的目标 Makefile执行的具体过程如下图:
4、Makefile编译过程
二、Makefile编写规则 1.# Makefile
2.SRCS = main.c 3.SRCS += command.c 4.SRCS += display.c 5.SRCS += files.c 6.SRCS += insert.c 7.SRCS += kbd.c 8.SRCS += search.c 9.SRCS += utils.c 10.# 语法替换
11.OBJS = $(SRCS:.c=.o) 12.DEPS = $(SRCS:.c=.d)
13.# 第一个目标 all,终极目标 14.all : edit
15.edit : $(OBJS) 16. $(CC) -o edit $(OBJS)
17.-include $(DEPS) 18.# 包含触发下面的 DEPS 依赖 19.$(DEPS) : %.d : %.c 20. @set -e; rm -f $@;\ 21. $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\ 22. sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\ 23. rm -f $@.$$$$
24..PHONY : clean 25.clean : 26. @-rm edit $(OBJS) $ (DEPS)
1、在规则中使用通配符 如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。 make支持三个通配符:“*”,“?”,“%”和“~”。这是和Unix的B-Shell是相同的。 波浪号(“~”)字符在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME目录下的test目录。而 “~hchen/test”则表示用户hchen的宿主目录下的test目录。 通配符(“*”)用于查找系统目录下的所有匹配项。如“*.c”表示所有后缀为c的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如:“*”,那么可以用转义字符“\”,如“\*”来表示真实的“*”字符,而不是任意长度的字符串。 通配符(“%”)用于匹配零个或多个字符。 通配符(“?”)用于匹配一个字符。
2、Make搜寻文件 实际中,比较大的工程文件都会分类放在不同目录下,当 Make 需要寻找文件依赖关系的时候,需要告知去寻找的路径,否则 make 只会查找当前目录。 两种方法: VPATH (变量) 1.VPATH = src:../inc 如上,指定了 ./src 和 ../inc 两个目录,冒号分隔,当前目录 搜索不到依赖文件的情况下,Make 就会依顺序进行搜索。 vpath (关键字)
注意:这不是一个变量,按照使用方式可以多次调用设定文件的搜索模式。
vpath 使用的三种方法
(1)vpath <pattern> <directories>:为符合模式<pattern>的文件指定搜索目录<directories>。
(2)vpath <pattern>:清除符合模式<pattern>的文件的搜索目录。
(3)vpath:清除所有已被设置好了的文件搜索目录。 vapth 使用方法中, <pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符。例如,“%.h”表示所有以“.h”结尾的文件。<pattern>指定了要搜索的文件集,而 <directories>则指定了<pattern>的文件集的搜索的目录。 1.vpath %.c dir1 # 在 dir1 寻找 .c 文件 2.vpath % dir2 # 在 dir2 寻找 任何需要的文件 3.vpaht %.c dir3 # 同 1 4.# 当前目录找不到的情况下i, 按照 dir1.2.3的顺序查找 .c 文件
3、伪目标 上面例子中,clean 就是一个伪目标。伪目标是一个标签,执行一些动作,比如清除文件,安装程序等。因为没有依赖关系,所以 make 无法直接决定是否需要执行。我们显示地用 .PHONY来告诉 make 这是一个伪目标, 避免与实际目标命名冲突。
同运行程序的时候我们给个参数让程序执行特定动作一样,运行 make 时指定伪目标标签,指定执行对应的命令。就如上述例子,执行 make clean 时进行清理工作。 4、静态模式 对应多个目标对象,构建每个对象对应名称的依赖关系的规则。 如下例子 1.OBJS = aa.o bb.o cc.o 2.$(OBJS) : %.o : %.c 3. cc -c $< -o $@ 4.# 等同: 5.aa.o : aa.c 6.cc -c aa.c -o aa.o 7.bb.o : bb.c 8.cc -c bb.c -o bb.o 9.cc.o : cc.c 10.cc -c cc.c -o cc.o 上述例子,Make 从 OBJS 集合中获取符合 目标模式 %.o 的文件作为目标,依赖模式 %.c 取前面获取的“%.o”的“%” 部分作为自己的前缀。在文件很多的情况下,可以大大提高了书写效率。
5、自动生成依赖关系 如果在 main.c 中包含了 defs.h 文件,那么依赖关系上我们需要写上 defs.h,这样,当 defs.h 文件修改了(比如新定义了一个宏..),Make 才会重新执行依赖关系。但是对于一个文件包含什么头文件,对应修改 Makefile,这样是很难维护的。 C/C++ 编译器 -MM 功能可以自动找寻文件的包含 ,生成依赖关系。
执行: $ gcc -MM mian.c 输出:
main.o : main.c defs.h 因此,我们借助编译器帮我们自动生成依赖关系,并包含到 Makefile 中。 上述的 -include 把每个源文件对应的依赖 [.d] 文件(gcc -MM生成的依赖关系)包含进来,把 [.d] 文件的更新也纳入 Makefile 中,修改了某个文件的依赖关系,对应命令执行生成新的依赖文件。
6、命令回响
1.-include $(DEPS) 2.$(DEPS) : %.d : %.c 3.@set -e; rm -f $@;\ 4. $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\ 5. sed ‘s,\($*\)\.o[ :]*,\1.o $@ : ,g’ < $@.$$$$ > $@;\ 6. rm -f $@.$$$$ 7. rm -f $@.$$$$
在 Makefile 中执行如下命令, 1.echo 命令执行 终端会输出如下 : 1.echo 命令执行 2.命令执行 第一行是执行的命令完整打印(回响),第二行才是我们需要的输出的,关闭命令回响的方法是在该行命令前添加 @ 1.@echo 命令执行 如果 Make 执行时,带参数“-n”或“--just-print”,那么其只是显示命令,不会执行命令,这个功能有利于我们调试我们的 Makefile,看看我们书写的命令执行起来是什么样子的或是什么顺序的。 而 Make 带参数“-s”或“--slient”则是全面禁止命令的显示。
7、命令的依赖 shell 按顺序一条条执行规则指定的命令。但是如果需要让上一条命令的结果应用到下一条,需要用分号分隔命令并保证命令处于同一行。 假设在目录 /home/lcd/mf/ 下执行 Makefile 1.exec1 : 2. @cd /home/lcd/kk 3. @pwd 4.# show : /home/lcd/mf
5.exec2 : 6. @cd /home/lcd/kk; pwd 7.# show : /home/lcd/kk
exec1 执行的所在目录,因为上一条命令的结果没有应用到下一条。 8、忽略出错命令 一般情况,Make 会一条一条执行命令,当某条命令执行后出错, Make 会终止当前规则,这可能导致整个任务终止。
有时候执行一些命令无需考虑出错,比如某文件存在删除,不存在就不管等。 这种情况下不希望出错终止,可以在任务前添加一个减号 - 1.clean : 2. -rm -f *.o 一个全局方法是, Make 运行加上“-i”或是“--ignore-errors”参数,那么,Makefile 中所有命令都会忽略错误。如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。
Make 的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。
9、Makefile嵌套 对于一个比较大的工程,不同模块分类在不同目录,分别用一个 Makefile 进行管理,模块化编译,方便工程维护和保证 Makefile 的简洁。 例如,子目录 subdir 下有一个 Makefile 描述该目录模块的编译规则, 那么总控 Makefile 中调用子目录 Makefile 可以这么写: 1.subsystem : 2. cd subdir && $(MAKE)# 等价 3.subsystem : 4. $(MAKE) -C subdir 使用 $(MAKE) 宏定义,在某些情况下调用嵌套我们可以直接修改添加参数。另外,当运行 Make 时候添加诸如 ‘-t’ (‘--touch’), ‘-n’ (‘--just-print’), or ‘-q’ (‘--question’) z这些特殊选线时,使用 $(MAKE) 可以保证语句相当于在前面添加了 + 号 的作用(特殊命令,继续执行)。很正常,希望测试的时候命令不是真的执行,但是包含其他 Makefile 这种命令是例外,必须执行,不然 Makefile 就不完整了, 我是这么理解的。 上层 Makefile 中定义的变量是可以在被调用的下一层 Makefile 中使用的, 前提是该变量在上层中被显式暴露 export,同理,可以采用 unexport取消。 1.export OBJS # 传递 变量 OBJS 2.export # 不指定,全部传递 如此,在下面的 makefile 就可以直接使用了。但是如果下层目录已经定义了该变量,那么下层默认使用的是它自己定义的变量值,除非上层 makefile 在调用下层 makefile 时给参数 -e,则会强行覆盖。 两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中。 Make 命令中的有几个参数并不往下传递,它们是“-C”,“-f”,“-h”“-o”和“-W” 嵌套执行中, “-w”或是“--print-directory”会在 Make 的过程中让你看到目前的工作目录。
比如,如果我们的下级 Make 目录是/home/lcd/mf/subdir,如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到:
make: Entering directory '/home/lcd/mf/subdir'
而在完成下层 make 后离开目录时,我们会看到:
make: Leaving directory `/home/lcd/mf/subdir'
当你使用“-C”参数来指定 Make 下层 Makefile 时,“-w”会被自动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总是失效的。
三、常见的解析符 1、自动化变量 | | | | | 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2> | | | | | | | | | | 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID | | | | |
2、变量和赋值
3、内置函数 | | | cfiles := $(wildcard *.c),
作用是匹配当前目录(不包含子目录)下所有.c文件,每个文件以空格隔开,然后赋值给cfiles变量 | | objs := $(patsubst %.c,%.o,$(wildcard *.c))
作用是将当前目录(不包含子目录)下所有的.c文件替换成对应的.o文件,即将后缀为.c的文件替换为后缀为.o的文件,每个文件以空格隔开,然后赋值给objs变量。 | | path := $(abspath main.c)
作用是获取当前目录下main.c文件的绝对路径(含文件名,结果比如:/work/main.c),然后赋值给path变量。 |
|