diff --git a/source/_posts/makefile/makefile_commands.md b/source/_posts/makefile/makefile_commands.md new file mode 100644 index 0000000..4486578 --- /dev/null +++ b/source/_posts/makefile/makefile_commands.md @@ -0,0 +1,156 @@ +--- +title: 跟我一起写Makefile-Makefile书写命令 +date: 2024-09-18 22:36:24 +category: makefile +tags: makefile +--- + +每条规则中的命令和操作系统Shell的命令行是一致的。make会一按顺序一条一条的执行命令,每条命令的开头必须以 Tab 键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。 + + + +# 显示命令 + +通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们把`@`字符放在命令行前时,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来向屏幕现实一些信息。如: + +```makefile +@echo 正在编译xxx模块... +``` + +当make执行时,会输出“正在编译xxx模块...”,但不会输出命令,如果没有`@`,那么make将输出: + +```bash +echo 正在编译xxx模块... +正在编译xxx模块... +``` + +如果make执行时,带入了参数`-n`或`--just-print`,那么它只会显示命令,但不会执行命令,这个功能很有利于调试Makefile。而参数`-s`或`--silent`或`--quiet`则是全面禁止命令的显示。 + +--- + +# 命令执行 + +如果你要让上一条命令的结果作用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令在cd之后的基础上运行,那么你就不能把这两条命令写在两行,而是同一行,用分号分隔。例如: + +```makefile +exec: + cd /home/usr + pwd + +exec: + cd /home/usr; pwd +``` + +在第一个例子中,`cd`将不会生效,`pwd`会打印当前的Makefile目录,而在第二个例子中,`cd`就会起作用了,`pwd`会打印出`/home/usr`。 + +--- + +# 命令出错 + +如果某个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。 + +有些时候命令的出错并不代表着执行是错误的,例如`mkdir`命令,如果目录已经存在也会出错。为了忽略命令的错误,可以在Makefile的命令行前面加一个`-`号,标记为不管命令是否出错都认为是成功的。例如: + +```makefile +clean: + -rm -f *.o +``` + +还有一个全局的方法是,给make加上`-i`或是`--ignore-errors`参数,那么Makefile中的所有命令都会忽略错误。而如果一个规则是以`.IGNORE`为目标的,那么这个规则中的所有命令将会忽略错误。 +还有一个参数是`-k`或是`--keep-going`,表示如果某规则中的命令出错了,那么终止该规则的执行,但继续执行其它规则。 + +--- + +# 嵌套执行make + +在一些大工程中,我们会把不同模块或者是不同功能的源文件放在不同的目录下,并在每个目录中都写一个该目录的Makefile,这样更有利于维护。 +例如,我们有一个子目录叫subdir,这个目录下有一个Makefile文件,来指明这个目录下文件的编译规则。那么我们总控的Makefile可以这样写: + +```makefile +subsystem: + cd subdir && $(MAKE) + +或 + +subsystem: + $(MAKE) -C subdir +``` + +定义MAKE宏变量的意思是也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入subdir目录,然后执行make命令。 +我们把顶层的Makefile叫做“总控Makefile”,这个Makefile中的变量可以传递到下级的Makefile当中,但是不会覆盖下级中定义的变量,除非指定了`-e`参数。 + +如果要传递变量到下级Makefile中,可以这样声明: + +```makefile +export ; +``` + +如果不想传递,则可以这样声明: + +```makefile +unexport ; +``` + +对于: + +```makefile +export variable = value + +variable = value +export variable + +export variable := value + +variable := value +export variable +``` + +它们都是等价的。 +**如果需要传递所有变量,直接`export`即可,不需要跟随变量,表示传递所有。** + +需要注意的是,有两个变量:`SHELL`和`MAKEFLAGS`,这两个变量**无论是否export,都要传递到下层的Makefile中**,特别是`MAKEFLAGS`,其中包含了make的参数信息,如果我们执行“总控Makefile”的时候有make参数或者是在上层的Makefile中定义了这个变量,那么`MAKEFLAGS`变量中将包含这些参数,并向下传递,这是一个系统级别的环境变量。 + +但是make命令中的有几个参数并不会向下传递,它们是`-C`,`-f`,`-h`,`-o`和`-W`,如果你不想往下层传递参数,那么可以这样: + +```makefile +subsystem: + cd subdir && $(MAKE) MAKEFLAGS= +``` + +如果定义了`MAKEFLAGS`,那么你就得确信其中的选项是大家都会用到的。 + +还有一个在“嵌套执行”中比较有用的参数,`-w`或者是`--print-directory`会在make的执行过程中输出一些信息,让你看到目前的工作目录。比如,当我们的下级make目录是“/home/usr/gnu/make”时,如果我们使用`make -w`来执行,那么当进入该目录时,我们会看到: + +```makefile +make: Entering directory '/home/usr/gnu/make' +``` + +make完成后离开时,会看到: + +```makefile +make: Leaving directory '/home/usr/gnu/make' +``` + +当你使用`-C`参数来指定make下层的Makefile时,`-w`会被自动打开。如果参数中有`-s`(`--silent`)或者是`--no-print-directory`,那么`-w`总是失效的。 + +--- + +# 定义命令包 + +如果Makefile中出现一些相同的命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以`define`开始,以`endf`结束,如: + +```makefile +define run-yacc +yacc $(firstword $^) +mv y.tab.c $@ +endef +``` + +这里,“run-yacc”是这个命令包的名字,**不要和Makefile中的变量重名**。在`define`和`endef`之间的就是命令序列。 + +要使用这个命令包的话,像是使用变量一样即可: + +```makefile +foo.c : foo.y + $(run-yacc) +``` \ No newline at end of file diff --git a/source/_posts/makefile/makefile_rules.md b/source/_posts/makefile/makefile_rules.md new file mode 100644 index 0000000..ff497e6 --- /dev/null +++ b/source/_posts/makefile/makefile_rules.md @@ -0,0 +1,340 @@ +--- +title: 跟我一起写Makefile-Makefile书写规则 +date: 2024-09-18 22:34:49 +category: makefile +tags: makefile +--- + +规则包含两个部分,一个是依赖关系,一个是生成目标的方法。 + + + +在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的。一般来说,定义在Makefile中的目标可能有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么第一个目标会成为最终的目标。make所完成的也就是这个目标。 + +# 规则的语法 + +```makefile +targets : prerequisites + command + ... +``` + +或是这样: + +```makefile +targets : prerequisites ; command + command + ... +``` + +targets是文件名,以空格分开,也可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。 + +command是命令行,如果其不与“target : prerequisites”在一行,那么,必须以`Tab`键开头,如果在一行,那么可以用分号作为分隔。 + +一般来说,make会以UNIX的标准Shell,也就是`/bin/sh`来执行命令。 + +--- + +# 在规则中使用通配符 + +如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make支持三个通配符:`*`、`?`和`~`。这是和Unix的B-Shell是相同的。 + +```makefile +objects = *.o +``` + +在这个例子中,表示了通配符同样可以使用在变量中,并不是说`*.o`会展开,不!objects的值就是`*.o`。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有`.o`文件的集合,那么你可以这样: + +```makefile +objects := $(wildcard *.o) +``` + +另外给一个变量使用通配符的例子: + +1. 列出一确定文件夹中的所有`.c`文件: + +```makefile + objects := $(wildcard *.c) +``` + +2. 列出(1)中所有文件对应的`.o`文件: + +```makefile + $(patsubst %.c,%.o,$(wildcard *.c)) +``` + +3. 由(1)(2)两步,可以写出编译并链接所有`.c`和`.o`文件: + +```makefile + objects := $(patsubst %.c,%.o,$(wildcard *.c)) + foo : $(objects): + cc -o foo $(objects) +``` + +其中`wildcard`、`patsubst`是Makefile中的关键字,我们将在后面讨论。 + +--- + +# 文件搜寻 + +在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件进行分类,并存放在不同的目录中。所以,当make需要去寻找文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make自动去找。 + +Makefile文件中的特殊变量`VPATH`就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去寻找依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去寻找文件了。 + +```makefile +VPATH = src:../headers +``` + +上面的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方) + +另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种: + +`vpath `:为符合模式\的文件指定搜索目录\。 + +`vpath `:清除符合模式\的文件的搜索目录。 + +`vpath`:清除所有已被设置好了的文件搜索目录。 + +vpath使用方法中的\需要包含`%`字符,`%`的意思是匹配零或若干字符,(需引用 `%` ,使用 `\` )例如, `%.h` 表示所有以 `.h` 结尾的文件。\指定了要搜索的文件集,而\则指定了\的文件集的搜索的目录。例如: + +vpath %.h ../headers + +该语句表示,要求make在“../headers”目录下搜索所有以 `.h` 结尾的文件。(如果某文件在当前目录没有找到的话) + +我们可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的\ ,或是被重复了的\,那么,make会按照vpath语句的先后顺序来执行搜索。如: + +```makefile +vpath %.c foo +vpath % blish +vpath %.c bar +``` + +其表示 `.c` 结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。 + +```makefile +vpath %.c foo:bar +vpath % blish +``` + +而上面的语句则表示 `.c` 结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。 + +--- + +# 伪目标 + +最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”, + +```makefile +clean: + rm *.o temp +``` + +因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。 + +当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。 + +```makefile +.PHONY : clean +``` + +只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只需要执行`make clean`。于是整个过程可以这样写: + +```makefile +.PHONY : clean +clean : + rm *.o temp +``` + +伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性: + +```makefile +all : prog1 prog2 prog3 +.PHONY : all + +prog1 : prog1.o utils.o + cc -o prog1 prog1.o utils.o + +prog2 : prog2.o + cc -o prog2 prog2.o + +prog3 : prog3.o sort.o utils.o + cc -o prog3 prog3.o sort.o utils.o +``` + +我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于默认目标的特性是,总是被执行的,但由于“all”又是一个伪目标,伪目标只是一个标签不会生成文件,所以不会有“all”文件产生。于是,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。 `.PHONY : all` 声明了“all”这个目标为“伪目标”。(注:这里的显式“.PHONY : all” 不写的话一般情况也可以正确的执行,这样make可通过隐式规则推导出, “all” 是一个伪目标,执行make不会生成“all”文件,而执行后面的多个目标。建议:显式写出是一个好习惯。) + +随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子: + +```makefile +.PHONY : cleanall cleanobj cleandiff + +cleanall : cleanobj cleandiff + rm program + +cleanobj : + rm *.o + +cleandiff : + rm *.diff +``` + +“make cleanall”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。 + +--- + +# 多目标 + +Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的生成规则的执行命令不是同一个,这可能会给我们带来麻烦,不过好在我们可以使用一个自动化变量 `$@` (关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。 + +```makefile +bigoutput littleoutput : text.g + generate text.g -$(subst output,,$@) > $@ +``` + +上述规则等价于: + +```makefile +bigoutput : text.g + generate text.g -big > bigoutput +littleoutput : text.g + generate text.g -little > littleoutput +``` + +其中, `-$(subst output,,$@)` 中的 `$` 表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是替换字符串的意思, `$@` 表示目标的集合,就像一个数组, `$@` 依次取出目标,并执行命令。 + +--- + +# 静态模式 + +静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活: + +```makefile + : : + + ... +``` + +targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。 + +target-pattern是指明了targets的模式,也就是的目标集模式。 + +prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。 + +这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的\定义成 `%.o` ,意思是我们的\;集合中都是以 `.o` 结尾的,而如果我们的\定义成 `%.c` ,意思是对\所形成的目标集进行二次定义,其计算方法是,取\模式中的 `%` (也就是去掉了 `.o` 这个结尾),并为其加上 `.c` 这个结尾,形成的新集合。 + +所以,我们的“目标模式”或是“依赖模式”中都应该有 `%` 这个字符,如果你的文件名中有 `%` 那么你可以使用反斜杠 `\` 进行转义,来标明真实的 `%` 字符。 + +看一个例子: + +```makefile +objects = foo.o bar.o + +all: $(objects) + +$(objects): %.o: %.c + $(CC) -c $(CFLAGS) $< -o $@ +``` + +上面的例子中,指明了我们的目标从\$object中获取, `%.o` 表明要所有以 `.o` 结尾的目标,也就是 `foo.o bar.o` ,也就是变量 `$object` 集合的模式,而依赖模式 `%.c` 则取模式 `%.o` 的 `%` ,也就是 `foo bar` ,并为其加下 `.c` 的后缀,于是,我们的依赖目标就是 `foo.c bar.c` 。而命令中的 `$<` 和 `$@` 则是自动化变量, `$<` 表示第一个依赖文件, `$@` 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则: + +```makefile +foo.o : foo.c + $(CC) -c $(CFLAGS) foo.c -o foo.o +bar.o : bar.c + $(CC) -c $(CFLAGS) bar.c -o bar.o +``` + +试想,如果我们的 `%.o` 有几百个,那么我们只要用这种很简单的“静态模式规则”就可以写完一堆规则。“静态模式规则”的用法很灵活,如果用得好,那会是一个很强大的功能。再看一个例子: + +```makefile +files = foo.elc bar.o lose.o + +$(filter %.o,$(files)): %.o: %.c + $(CC) -c $(CFLAGS) $< -o $@ +$(filter %.elc,$(files)): %.elc: %.el + emacs -f batch-byte-compile $< +``` + +`$(filter %.o,\$(files))`表示调用Makefile的filter函数,过滤“\$files”集,只要其中模式为“%.o”的内容。 + +--- + +# 自动生成依赖性 + +在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句 `#include "defs.h"` ,那么我们的依赖关系应该是: + +```makefile +main.o : main.c defs.h +``` + +但是,如果是一个比较大型的工程,你必须清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,如果我们执行下面的命令: + +```bash +cc -M main.c +``` + +其输出是: + +```makefile +main.o : main.c defs.h +``` + +于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用 `-MM` 参数,不然, `-M` 参数会把一些标准库的头文件也包含进来。 + +`gcc -M main.c`的输出是: + +```makefile +main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \ + /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \ + /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \ + /usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \ + /usr/include/bits/sched.h /usr/include/libio.h \ + /usr/include/_G_config.h /usr/include/wchar.h \ + /usr/include/bits/wchar.h /usr/include/gconv.h \ + /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \ + /usr/include/bits/stdio_lim.h +``` + +`gcc -MM main.c`的输出则是: + +```makefile +main.o: main.c defs.h +``` + +那么,编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来,我们的Makefile也要根据这些源文件重新生成,让 Makefile 自己依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个 `name.c` 的文件都生成一个 `name.d` 的Makefile文件, `.d` 文件中就存放对应 `.c` 文件的依赖关系。 + +于是,我们可以写出 `.c` 文件和 `.d` 文件的依赖关系,并**让make自动更新或生成 `.d` 文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了**。 + +这里,我们给出了一个模式规则来产生 `.d` 文件: + +```makefile +%.d: %.c + @set -e; rm -f $@; \ + $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ + sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ + rm -f $@.$$$$ +``` + +这个规则的意思是,所有的 `.d` 文件依赖于 `.c` 文件, `rm -f $@` 的意思是删除所有的目标,也就是 `.d` 文件,第二行的意思是,为每个依赖文件 `$<` ,也就是 `.c` 文件生成依赖文件, `$@` 表示模式 `%.d` 文件,如果有一个C文件是name.c,那么 `%` 就是 `name` , `$$$$` 意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。 + +总而言之,这个模式要做的事就是**在编译器生成的依赖关系中加入 `.d` 文件的依赖**,即把依赖关系: + +```makefile +main.o : main.c defs.h +``` + +转成: + +```makefile +main.o main.d : main.c defs.h +``` + +于是,我们的 `.d` 文件也会自动更新了,并会自动生成了,当然,你还可以在这个 `.d` 文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个 `.d` 文件都包含一个完整的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如: + +```makefile +sources = foo.c bar.c + +include $(sources:.c=.d) +``` + +上述语句中的 `$(sources:.c=.d)` 中的 `.c=.d` 的意思是做一个替换,把变量 `$(sources)` 所有 `.c` 的字串都替换成 `.d` ,关于这个“替换”的内容,在后面会有更为详细的讲述。当然,需要注意次序,因为include是按次序来载入文件,最先载入的 `.d` 文件中的目标会成为默认目标。