一、读懂Makefile

1、Makefile简介

简单来说,Makefile就是帮助我们编译工程(文件)并生成可执行文件(目标文件)。现在的IDE以及一些自动化编译工具基本上都做了Makefile的事,并不需要我们自己去手动编写Makefile,但是在Linux环境下,对于一个大型工程来说,我们常常要定制自己的编译规则,要编译哪些文件,该如何编译,编译结果如何处理,通过Makefile就可以搞定。有了Makefile,只需要执行一个简单的make命令,即可实现自动化编译,当然有时候执行make命令之前还需要做一些环境配置工作,比如在android环境下开发时,首先要通过source或句点命令执行shell脚本“build/envsetup.sh”并lunch一个产品选项,这个根据实际情况而不同。

在Linux下通过gcc进行编译时,包括四个阶段:预编译、编译、汇编和链接,分别生成“.i”、“.s”、“.o”和“可执行文件”。前三个阶段可归结为一个过程,即编译过程以生成中间文件“.o”,链接过程有两种情况,静态链接和动态链接,也就是所谓的库文件,静态库后缀为“.a”,动态库后缀为”.so”。了解gcc的编译过程,有助于理解Makefile,下面先来看一个简单的例子。

2、第一个Makefile

以c代码为例,这里有四个文件,分别如下。

main.c——调用circle.h头文件声明的length()和area()函数

#include "circle.h"
#include <stdio.h>int main(void)
{printf("length = %lf\n", length(5));printf("area = %lf\n", area(5));return 0;
}

circle.h——声明length()和area()函数

#ifndef CIRCLE_H
#define CIRCLE_H double length(int r);
double area(int r); #endif // CIRCLE_H

circle.c——实现length()和area()函数

#include "circle.h"
#include "def.h" double length(int r)
{ return 2 * PI * r;
} double area(int r)
{ return PI * r * r;
}

def.h——圆周率PI的定义

#ifndef DEF_H
#define DEF_H #define PI 3.1415926 #endif // DEF_H

使用gcc编译以上四个文件,假设目标文件为main,命令如下:

$gcc -o main main.c circle.c

当工程中有n个文件时,还是像上面的命令一样在gcc后面跟着一大堆文件吗,这样就太笨拙了,也不现实。即便如此,如果只是某个文件修改了一点代码,也要编译整个工程吗,想想就不必这样,有时候我们还要单独编译某个功能模块,这时候就该Makefile发挥作用了。编译上面的四个文件,可使用如下Makefile。

Makefile——

main: main.o circle.o gcc -o main main.o circle.o main.o: main.c circle.h gcc -c main.c circle.o: circle.c circle.h def.h gcc -c circle.c clean: rm main main.o circle.o

上面的Makefile是一个非常简单的文件,通过这个文件,只需要执行命令“make”就可以生成目标文件main,执行命令“make clean”就可以删除目标文件main和一些“.o”中间文件,当工程庞大时Makefile的好处就显现出来了,其神奇效果如下:

(1)如果这个工程没有编译过,那么所有的c文件都要被编译和链接。
(2)如果这个工程的某几个c文件被修改了,那么只编译被修改的这几个c文件并链接目标程序。
(3)如果这个工程的某几个头文件被修改了,那么只编译引用了这几个头文件的c文件并链接目标程序。

从上面的Makefile可以看出其一般格式如下:

target: prerequisites commands

或者:

target: prerequisites; commands

target是目标文件,后面跟着一个冒号,prerequisites是生成target所需要的依赖文件,当文件较多时,可使用续行符号“\”进行续行,续行符后面不能有空格,另起一行的commands是make执行的shell命令,有一点必须注意,commands前要留一个Tab键的距离。如果它们在同一行的话commands前以分号进行分隔。Makefile支持通配符,例如“*”,用法同bash,通配符只可以在目标、依赖、命令中使用,其它地方使用时,如变量赋值,可使用wildcast函数。

3、Makefile工作过程

(1)一般情况下,当文件名为“Makefile”或“makefile”时,也可以是“GNUmakefile”,只需要输入命令“make”即可,这三个文件的执行顺序依次为“GNUmakefile”、“makefile”、“Makefile”,但有时候我们要起一个一目了然的文件名,以清楚地知道这个文件是干什么用的,这时执行make命令时就要使用“-f”参数了,以指定具体的文件,例如“make -f main.mk”。

(2)在Makefile中可以引用外部的Makefile,语法是“include filename”,类似于C/C++的“#include”,支持绝对路径和相对路径。make命令执行时,默认在当前目录下查找Makefile,还可以通过“-I”参数指定查找目录。

(3)找到Makefile后,它会找文件中的第一个目标文件,即文件中最顶端的target,这个target编译完成后则整个编译过程结束,如果还有其他的target没有编译也不再编译,所以文件中的第一个target格外重要。但是,有时候我们偏偏只要编译文件中的某个target,这也是可以的,只需要在make命令后输入这个target即可,其它不相关的target将不再编译。

(4)整个编译过程其实就是文件依赖的查找与编译的过程。当目标文件不存在或者依赖文件比目标文件新时,将执行后面的commands命令,然后当前的依赖文件又会去查找它自己的依赖文件,就这样一层一层执行下去。

(5)上面例子中Makefile的clean,它没有被第一个target关联,也没有被其它的target关联,是不会被自动执行的,不过可以像第“(2)”点提到的,使用命令“make clean”来完成。

Makefile执行过程可以简单的概括为:Makefile读取>include的Makefile读取>变量初始化>隐式规则自动推导>目标文件创建依赖关系>根据依赖关系决定哪些目标文件需要重新生成>执行生成命令。

4、使用变量来优化Makefile

在上面的例子中,main这个target用到了main.o和circle.o两次,clean这个target也用到了main.o和circle.o一次,这还算好的,如果依赖文件多时,写多次就太麻烦了,我们可以使用变量来简化这个过程,类似于shell脚本中的变量,修改上面的Makefile如下。

objects = main.o circle.o main: $(objects) gcc -o main $(objects) main.o: main.c circle.h gcc -c main.c circle.o: circle.c circle.h def.h gcc -c circle.c clean: rm main $(objects)

上面例子中,我们把main.o和circle.o赋值给了变量objects,这个变量名字是可以随意起的,大小写敏感,可以是字母、数字和下划线,访问变量时使用美元符号“$”,并把变量用圆括号括起来,使用花括号也是可以的,在下面的介绍中统一使用圆括号。

Makefile中的变量可分为两类,取决于Makefile的两个执行过程。第一阶段:读取所有的Makefile 文件,包括“MAKIFILES”变量指定的、关键字“include”指定的、以及命令行选项“-f(–file)”指定的Makefile 文件,内建所有的变量、显式规则和隐式规则,并建立所有目标和依赖之间的依赖关系结构链表。第二阶段:根据第一阶段已经建立的依赖关系结构链表决定哪些目标需要更新,并使用对应的规则来重建这些目标。在make执行的第一阶段中如果变量和函数被展开,那么称此展开是“立即”的,此时所有的变量和函数被展开在需要构建的结构链表的对应规则中;其它的展开称之为“延后”的,这些变量和函数不会被“立即”展开,而是直到后续某些规则需要使用时或者在make处理的第二阶段它们才会被展开。对应的,Makefile中的变量定义有以下几种不同的形式。

IMMEDIATE = DEFERRED
IMMEDIATE ?= DEFERRED
IMMEDIATE := IMMEDIATE
IMMEDIATE += DEFERRED or IMMEDIATE
define IMMEDIATE
DEFERRED
endef 

符号“=”表示引用右边变量的最新值,递归展开,符号“:=”表示引用右边变量的不超过引用位置的最新值,直接展开,符号“?=”表示左边变量未定义时才进行赋值,符号“+=”表示追加变量值,关键字define与endef中间的内容可以理解为定义了一个组合命令或函数。

下面从立即变量与延后变量的角度再来看一下目标依赖规则:

IMMEDIATE: IMMEDIATE
DEFERRED

5、使用make的自动推导功能(隐式规则)来优化Makefile

make可以自动推导文件依赖及文件依赖关系后面的命令,也就是常说的隐式规则,与显式规则相比,这样就又可以少写几行代码,以“circle.o”为例,make会自动推导出依赖文件“circle.c”和命令“cc -c -o circle.o circle.c”,如果有其它的依赖文件,只需列出它们即可,再次修改上面的Makefile如下。

objects = main.o circle.o main: $(objects) gcc -o main $(objects) main.o: circle.h
circle.o: circle.h def.h clean: rm main $(objects)

另外,当工作目录没有任何Makefile、makefile、GNUmakefile时,也可以使用隐式规则进行编译。如果有一个文件“circle.c”,执行命令“make circle.o”,生成“circle.o”,实际执行的是“cc -c -o circle.o circle.c”。对于Makefile的隐式规则,有其隐式的目标依赖关系,比如目标“foo”依赖于文件“foo.o”,而“foo.o”又依赖于“foo.c”,它们根据后缀规则执行合适的命令以生成目标文件。make的默认后缀列表为:“.out”、“.a”、“.ln”、“.o”、“.c”、“.cc”、“.C”、“.p”、“.f”、“.F”、“.r”、“.y”、“.l”、“.s”、“.S”、“.mod”、“.sym”、“.def”、“.h”、“.info”、“.dvi”、“.tex”、“.texinfo”、“.texi”、“txinfo”、“.w”、“.ch”、“.web”、“.sh”、“.elc”、“el”。

6、伪目标

每个Makefile一般都有一个clean操作,用于清除文件,一般在文件最下面,常见的做法是使用“.PHONY”把clean变为一个伪目标,因为clean并不是一个真正的目标文件,也可以理解为一个标签,修改上面Makefile如下。

objects = main.o circle.o main: $(objects) gcc -o main $(objects) main.o: circle.h
circle.o: circle.h def.h .PHONY: clean
clean: -rm main $(objects)

仔细观察可以发现rm命令前面有个减号,意思是命令执行过程中如果遇到某些错误将忽略,继续执行。

伪目标的另一种功能:执行“make”时,编译的是文件最顶端的终极目标文件,如果目标文件有多个,终极目标文件也是只有一个,即第一个目标文件,如果我们想一口气编译多个目标文件,就可以使用伪目标,把伪目标放到文件顶端,伪目标的依赖文件为我们真正想要的目标文件。默认情况下,终极目标就是出现在Makefile中,除了以点号“.”开始的第一个规则中的第一个目标(如果第一个规则存在多个目标)。伪目标和空目标有一些常用的名字:all、clean、mostlyclean、distclean、realclean、clobber、install、print、tar、shar、dist、TAGS、check、test。

7、多目标与静态模式

通过上面的Makefile可以发现,目标文件都是一个,依赖文件则有多个,也就是一对一或一对多的关系,其实多个目标文件也可以写在一起,也就是多对一或多对多的关系。其实,使用自动化变量“$@”可以给不同的目标使用不同的命令,但给不同的目标使用不同的依赖就要用到静态模式了,静态模式可以更容易地定义多目标的规则,语法如下:

targets: <targets-pattern>: <prerequisites-pattern>

targets指定了一系列的目标文件,是一个目标文件的集合。targets-pattern指明了targets的模式。prerequisites-pattern指明了依赖文件的模式,与targets-pattern相关。

下面以一个例子说明,假设我们有dog.c和cat.c两个文件,想要编译生成dog.o和cat.o,Makefile如下:

objects = dog.o cat.o all: $(objects) $(objects): %.o: %.c gcc -c $< -o $@ .PHONY: clean
clean: -rm *.o

上面的Makefile,最终目标是all,为多目标,模式“%.o”表示目标为以“.o”结尾的文件即dog.o和cat.o,“%.c”则是将“%.o”表示的文件的后缀从“.o”替换为“.c”,“$<”“$@”是两个自动化变量,分别表示第一个依赖文件和目标文件。

8、双冒号规则

双冒号规则就是使用“::”代替普通规则的“:”得到的规则。当同一个文件作为多个规则的目标时(同时为普通规则或者同时为双冒号规则),双冒号规则的处理和普通规则的处理完全不同。 双冒号规则中,当依赖文件比目标更新时,规则将会被执行。对于一个没有依赖而只有命令行的双冒号规则,当引用此目标时,规则的命令将会被无条件执行;而普通规则,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。当同一个文件作为多个双冒号规则的目标时,这些不同的规则会被独立的处理;而普通规则会合并所有的依赖到一个目标文件。这就意味着对多个双冒号规则的处理就像多个不同的普通规则一样,多个双冒号规则中的每一个的依赖文件被改变之后,make只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执行。

9、“.d”文件(自动生成依赖)

在Makefile中,依赖文件可能会包含多个头文件,在一个大型的工程中,我们必须知道源文件中包含了哪些头文件,这样才能编写依赖关系,数量庞大时就显得工作量繁重了,而且可维护性差,一个好的做法是使用gcc编译的“-M”或者“-MM”选项,它会自动生成依赖关系,这两个选项的区别是前者包含了标准库的头文件而后者没有。以我们一开始的main.c为例,结果如下:

$gcc -M main.c
main.o: main.c circle.h /usr/include/stdio.h /usr/include/features.h \ /usr/include/stdc-predef.h /usr/include/x86_64-linux-gnu/bits/predefs.h \ /usr/include/x86_64-linux-gnu/sys/cdefs.h \ /usr/include/x86_64-linux-gnu/bits/wordsize.h \ /usr/include/x86_64-linux-gnu/gnu/stubs.h \ /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \ /usr/lib/gcc/x86_64-linux-gnu/4.6/include/stddef.h \ /usr/include/x86_64-linux-gnu/bits/types.h \ /usr/include/x86_64-linux-gnu/bits/typesizes.h /usr/include/libio.h \ /usr/include/_G_config.h /usr/include/wchar.h \ /usr/lib/gcc/x86_64-linux-gnu/4.6/include/stdarg.h \ /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \ /usr/include/x86_64-linux-gnu/bits/sys_errlist.h $gcc -MM main.c
main.o: main.c circle.h 

从上面的例子可以看出“-M”与“-MM”的区别,我们一般使用后者。自动生成的依赖关系放在“.d”文件中,例如main.c的依赖关系对应于main.d,且看下面的Makefile。

sources:=$(wildcard *.c)
objects=$(sources:.c=.o) all: main %.d: %.c rm -f $@ gcc -MM $< > tmp sed 's,\($*\)\.o,\1.o $@,g' < tmp > $@ rm -f tmp sinclude $(sources:.c=.d) main: $(objects) gcc -o main $^ .PHONY: clean
clean: -rm -f main *.o *.d

上面的Makefile涉及的知识点较多,稍微有点复杂,下面详细解释一下。

第一行:“:=”表示赋值,Makefile支持通配符,但是给变量赋值时“*.c”并不会被展开,表示的还是其字面值,所以要用“wildcard”函数进行转换。
第二行:字符串替换,把“sources”中的“.c”替换为“.o”,同样的功能可以使用Makefile中的patsubst函数实现。
第三行:空白行。
第四行:目标文件为“all”,依赖文件为“main”。
第五行:空白行。
第六行:目标文件为“%.d”,依赖文件为“.c”,“sources”中所有的“.c”文件都会生成一个对应的“.d”文件。
第七行:删除旧的目标文件,目的是准备生成新的目标文件,自动变量“$@”表示目标文件。
第八行:把上面提到的自动生成的文件依赖关系输入到“tmp”临时文件,“-MM”就是用来自动生成依赖关系但不包括标准库,“$<”表示第一个依赖文件,“>”是数据流重定向符号。
第九行:使用了sed命令,功能是把临时文件“tmp”中的“xxx.o”替换为“xxx.o xxx.d”,即增加一个“.d”文件,xxx是一个“.c”文件的名字,这里还用到了shell的数据流输入输出重定向。
第十行:删除tmp临时文件。
第十一行:空白行。
第十二行:使用了“sinclude”,用法同“-include”,include前面的减号表示出错时继续执行,这里使用include也可以,sinclude是为了提高Makefile的兼容性。
第十三行:空白行。
第十四行:“main”是最后生成的可执行文件,依赖于“objects”。
第十五行:使用了自动变量“$^”,表示依赖文件列表。
第十六行:空白行。
第十七行:伪目标。
第十八行:clean,没有依赖。
第十九行:使用rm命令执行一些真正的清除工作。

10、模式规则与后缀规则

上面的例子中用到了模式规则,模式规则需要使用百分号“%”,可以匹配任意字符串或文件名。后缀规则是一种古老的定义隐式规则的方式,在新版本的make中使用模式规则作为对它的替代,模式规则相比后缀规则更加清晰明了。在现在版本中保留它的原因是为了能够兼容旧的Makefile文件。后缀规则有两种类型:“双后缀”和“单后缀”。双后缀规则定义一对后缀,目标文件的后缀和依赖目标的后缀,它匹配所有后缀为目标后缀的文件,对于一个匹配的目标文件,它的依赖文件这样形成:将匹配的目标文件名中的后缀替换为依赖文件的后缀得到。如:一个描述目标和依赖后缀的“.o”和“.c”的规则就等价于模式规则“%o: %c”。单后缀规则只定义一个后缀,此后缀是源文件名的后缀,它可以匹配任何文件,其依赖文件这样形成:将后缀直接追加到目标文件名之后得到。如:单后缀“.c”就等价于模式规则“% : %.c”。后缀规则没有任何依赖,但必须有命令。

11、目录搜索

在Makefile中的目标依赖文件查找过程中,首先在当前工作目录下查找,查找失败时,可在特殊变量VPATH指定的目录中进行查找,各个目录以空格或冒号分隔。还可以通过关键字vpath指定不同模式的搜索路径,有三种用法,如下:

vpath PATTERN DIRECTORY
vpath PATTERN
vpath

第一种用法为所有符合模式的文件指定搜索路径,第二种用法用于清除符合模式的文件的搜索路径,第三种用法则清除所有的已设置的文件搜索路径。对于库文件来说,优先搜索共享库,搜索失败时尝试搜索静态库。

12、Makefile注释

Makefile注释以“#”开始,shell脚本的注释也是如此,如果在文件中使用“#”字符就要进行转义“#”。

二、Makefile高级特性

1、特殊符号

“@”:执行make命令时,默认会把Makefile中的命令输出到屏幕,如果不想要这样的输出时,可以在Makefile文件中的命令前添加“@”符号,对应的命令就不会输出到屏幕。

“-”:在bash中执行命令时,都有一个回传码,0表示命令正确执行。前面我们介绍过减号“-”的用法,在一个命令的前面,表示这个命令出错时忽略出错信息,继续执行其它的命令。

“$”:在Makefile中美元符号“$”随处可见,访问变量或者调用函数,当使用真实的美元符号时,类似于转义字符,形式为“$$”

“+”:有时候,在命令前面还有个加号,命令行前的“+”的意思是告诉make,即使make使用了“-t”参数,“+”之后的命令都需要被执行。

2、使用make命令的参数

make命令的参数众多,可在shell中通过“man”来查看,这里简单介绍几个。

-n:只显示Makefile中的命令,但不真正执行,可查看Makefile的执行过程。
-s:不显示Makefile中的任何命令,但这些命令会执行到。
-i:忽略所有命令的错误。
-k:当前命令有错误时,继续执行其它的。
-w:执行Makefile时,输出目录信息。

3、嵌套执行make命令

大型工程中,代码一般会以功能模块划分,每个功能模块都会有自己的Makefile,例如当前目录有一个Makefile和subdir目录,subdir目录中又有一个Makefile,那么我们如何在当前目录的Makefile中直接执行subdir目录中的Makefile呢?下面两种方法都是可以的。

submodule: $(MAKE) -C subdir

或者:

submodule: cd subdir && $(MAKE) 

subdir这个子目录中的Makefile如下:

sub: @echo "Makefile in subdir"  

当我们“make submodule”时,subdir这个子目录中的Makefile就会执行,输出“Makefile in subdir”。

4、传递命令结果

如果想要上一条命令的结果作用下一条命令,那么这两条命令就应该写在一行,以分号分隔,如果这两条命令独立成行的话,结果就大不同了,因为命令以行为单位,在一个独立的子进程中执行,如下Makefile:

.PHONY: all
all: 

showcur: cd /home pwd showhome: cd /home; pwd

当我们“make showcur”时,pwd命令显示的是当前路径。
当我们“make showhome”时,pwd命令显示的是home路径。

5、传递变量

变量传递类似于bash中把当前shell的变量传递到其子shell,这里是传递到子目录中的Makefile,使用“export”关键字,不想传递时使用“unexport”。如下例子所示。

当前目录Makefile:

fathervar = "father"
export fathervar
submodule:   $(MAKE) -C subdir

subdir这个子目录的Makefile:

sub: @echo "Makefile in subdir" @echo $(fathervar)

如果想传递所有的变量,export后不加任何东西即可。

6、变量嵌套与拼接、变量替换

变量可以嵌套在一起使用,变量拼接用法同shell脚本,将变量直接连在一起写即可,如下Makefile:

A := a
B := b
ab := "vars"
multivar: echo $($(A)$(B))

输出结果是“vars”。

变量替换使用如下格式:

$(VAR:A=B)

替换变量“VAR”中所有“A”字符结尾的字为“B”结尾的字,“结尾”的含义是空格之前(变量值多个字之间使用空格分开),而对于变量其它部分的“A”字符不进行替换。例如:

foo := a.o b.o c.o
bar := $(foo:.o=.c)

变量“bar”的值为“a.c b.c c.c”。

7、空变量与空格

如下例子,nullstring为空变量,什么也没有,space为空格。

nullstring :=
space := $(nullstring)

8、局部变量与模式变量

上面Makefile的例子中我们定义的变量都是全局变量,我们还可以定义局部变量,也叫目标变量,例子如下:

globalvar := "global"
local: globalvar := "local"
local: echo $(globalvar)local2: echo $(globalvar)

globalvar是个全局变量,在local这个目标中进行了重定义,变成了局部变量,然后在local目标中访问时值为“local”,而local2中还是“global”。在定义局部变量(目标变量)时,可以使用下面提到的关键字override。

与局部变量用法类似的还有个模式变量,也就是说这些变量定义只适用于匹配特定模式的文件,如下例子,指定了所有“.o”文件的编译选项包含“-O”,不改变对其它类型文件的编译选项。

%.o : CFLAGS += -O 

9、特殊变量

“MAKEFILES”:make在执行时,首先读取的是环境变量“MAKEFILES”所指定的文件列表,之后才是工作目录下的Makefile文件,“include”所指定的文件是在make发现此关键字的时,暂停正在读取的文件而转去读取“include”所指定的文件。

“MAKEFILE_LIST”:make程序在读取多个Makefile文件时,包括由环境变量“MAKEFILES”指定的、命令行指定的、当前工作目录下默认的以及使用关键字“include”包含的,在对这些文件进行解析执行之前make读取的文件名将会被自动依次追加到变量“MAKEFILE_LIST”中。

“.VARIABLES”:此变量不能通过任何途经给它赋值,展开以后是此引用点之前、Makefile文件中所定义的所有全局变量列表,包括空变量(未赋值的变量)和make的内嵌变量,但不包含目标指定的变量,目标指定的变量值在特定目标的上下文有效。

“.LIBPATTERNS”:值为“lib%.so lib%.a”,用于库文件的展开,例如“-lC”展开为“libC.so libC.a”。

“SHELL”:shell路径。

“CURDIR”:make当前工作目录。

“MAKE”:值为“make”。

“MAKEFLAGS”:make命令参数。

“MAKEOVERRIDES”:命令行中变量定义。

“MAKELEVEL”:递归调用make深度。

“MAKCMDGOALS”:此变量记录了命令行参数指定的终极目标列表,没有通过参数指定终极目标时此变量为空。

下面是几个代表命令的变量——

AR:函数库打包程序,可创建静态库.a文档。默认是“ar”。

AS:汇编程序。默认是“as”。

CC: C编译程序。默认是“cc”。

CXX:C++编译程序。默认是“g++”。

CO:从RCS中提取文件的程序。默认是“co”。

CPP:C程序的预处理器(输出是标准输出设备)。默认是“$(CC) -E”

FC:编译器和预处理Fortran和Ratfor源文件的编译器。默认是“f77”。

GET:从SCCS中提取文件程序。默认是“get”。

LEX:将Lex语言转变为C或Ratfo的程序。默认是“lex”。

PC:Pascal语言编译器。默认是“pc”。

YACC:Yacc文法分析器(针对于C程序)。默认命令是“yacc”。

YACCR:Yacc文法分析器(针对于Ratfor程序)。默认是“yacc -r”。

MAKEINFO:转换Texinfo源文件(.texi)到Info文件程序。默认是“makeinfo”。

TEX:从TeX源文件创建TeX DVI文件的程序。默认是“tex”。

TEXI2DVI:从Texinfo源文件创建TeX DVI文件的程序。默认是“texi2dvi”。

WEAVE:转换Web到TeX的程序。默认是“weave”。

CWEAVE:转换C Web到TeX的程序。默认是“cweave”。

TANGLE:转换Web到Pascal语言的程序。默认是“tangle”。

CTANGLE:转换C Web 到 C。默认是“ctangle”。

RM:删除命令。默认是“rm -f”。

下面是几个代表命令参数的变量——

ARFLAGS:执行“AR”命令的命令行参数。默认值是“rv”。

ASFLAGS:执行汇编语器“AS”的命令行参数(明确指定“.s”或“.S”文件时)。

CFLAGS:执行“CC”编译器的命令行参数(编译.c源文件的选项)。

CXXFLAGS:执行“g++”编译器的命令行参数(编译.cc源文件的选项)。

COFLAGS:执行“co”的命令行参数(在RCS中提取文件的选项)。

CPPFLAGS:执行C预处理器“cc -E”的命令行参数(C 和 Fortran 编译器会用到)。

FFLAGS:Fortran语言编译器“f77”执行的命令行参数(编译Fortran源文件的选项) 。

GFLAGS:SCCS “get”程序参数。 LDFLAGS 链接器(如:“ld”)参数。

LFLAGS:Lex文法分析器参数。 PFLAGS Pascal语言编译器参数。

RFLAGS:Ratfor 程序的Fortran 编译器参数。

YFLAGS:Yacc文法分析器参数。

10、特殊目标

“.PHONY”: 目标“.PHONY”所有的依赖称为伪目标,当使用make命令指定此目标时,无条件执行这个目标所在规则定义的命令。

“.SUFFIXES” : 目标“.SUFFIXES”的所有依赖指出了一系列在后缀规则中需要检查的后缀名。

“.DEFAULT”:目标“.DEFAULT”所在规则定义的命令用于重建那些没有具体规则的目标,包括显式规则和隐式规则。比如说,一个文件作为某个规则的依赖,但却不是另外一个规则的目标时,make程序无法找到重建此文件的规则,这种情况下就执行“.DEFAULT”所指定的命令。

“.PRECIOUS”:目标“.PRECIOUS”的所有依赖文件在make过程中会被特殊处理,当命令在执行过程中被中断时,make不会删除它们,包括中间文件。

“.INTERMEDIATE”:目标“.INTERMEDIATE”的依赖文件在make时作为中间文件。

“.SECONDARY”:目标“.SECONDARY”的依赖文件作为中间文件,但这些文件不会被自动删除,如果没有任何依赖文件时,则将所有的文件作为中间文件。

“.DELETE_ON_ERROR”:目标“.DELETE_ON_ERROR”的依赖文件在创建时,如果对应的命令执行错误,将删除已被修改的目标文件。

“.IGNORE”:目标“.IGNORE”指定依赖文件时,将忽略创建这些依赖文件所执行命令的错误,当此目标没有依赖文件时,将忽略所有命令执行的错误。

“.LOW_RESOLUTION_TIME”:目标“.LOW_RESOLUTION_TIME”的依赖文件被make认为是低分辨率时间戳
文件,通常,文件的时间辍都是高分辨率的,make在处理依赖关系时,对规则的目标-依赖文件的时间戳进行比较,判断目标是否过期。

“.SILENT”:目标“.SILENT”的依赖文件在创建时,不打印重建此文件所执行的命令,若没有任何依赖文件,则告诉make在执行过程中不打印任何执行的命令。

“.EXPORT_ALL_VARIABLES”:目标“.EXPORT_ALL_VARIABLES”应该作为一个简单的没有依赖的目标,它的功能是将之后所有的变量传递给子make进程。

“.NOPARALLEL”:目标“.NOPARALLEL”的所有命令按照串行方式执行,即使存在make的命令行参数“-j”,也串行执行,但在递归调用的子make进程中,命令可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将被忽略。

11、特殊的目标依赖规则

Makefile中还有个不常用的特殊的目标依赖规则,格式如下:

target: normal-prerequisites | order-prerequisitescommands

normal-prerequisites即一般的目标依赖,符号“|”后面的是特殊的目标依赖,当target存在时,如果normal-prerequisites被修改则重建target,但order-prerequisites被修改时并不重建target,也就是说,在target存在的情况下,order-prerequisites不参与目标重建工作,只有当target不存在时才参与目标构建。

12、关键字override

如果变量是通过make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略,如果你想在Makefile中设置这类参数的值,那么,你可以使用“override”,基本语法如下:

override <variable> = <value>

override还可用于关键字define定义的变量。

13、关键字include

在一个Makefile中,可以包含其它的Makefile文件,使用include关键字:

include FILENAMES...
-include FILENAMES...
sinclude FILENAMES...

include前面可以有空格但不能是Tab键,与文件名以空格或Tab键分隔,有多个文件时,它们以空格或Tab键分隔。包含的文件不存在时会报错,在include前面添加减号可忽略这个错误,或者使用sinclude也可以忽略这个错误。文件名支持通配符,如星号“*”

需要注意的是,一个Makefile包含了另一个Makefile后,它们可能有同样的目标,然而编译规则却是不同的,这是不允许的,这种情况下,可以在需要include其它文件的Makefile中使用所有模式匹配,如下:

%: force
$(MAKE) -f Makefile $@
force: ;

所有模式匹配,使用了百分号“%”,可匹配任何一个目标,它的依赖是“force”(没有依赖或命令且目标不是一个存在的文件时,这个目标是一个强制目标,总是会执行),保证了即使目标文件
存在也会执行这个规则(文件已存在时,需根据它的依赖文件的修改情况决定是否重建这个目标文件),“force”规则中使用空命令是为了防止make程序试图寻找一个规则去创建目标“force”时,又使用了模式规则“%: force”而陷入死循环。

14、静态库

静态库文件也称为“文档文件”,它是一些“.o”文件的集合。在 Linux(Unix)中使用工具“ar”对它进行维护管理。它所包含的成员(member)是若干“.o”文件。一个静态库通常由多个“.o”文件组成,这些成员(“.o”文件)可独立的作为一个规则的目标,库成员作为目标时需要按照如下的格式来书写:

ARCHIVE(MEMBER) 

这种书写方式只能出现在规则的目标和依赖,不能出现在规则的命令行中。因为,绝大多数命令不支持这种语法,命令不能直接对库的成员进行操作。例如:下边的规则用于创建库“foolib”,并将“hack.o”成员加入到库:

foolib(hack.o) : hack.o
ar cr foolib hack.o 

三、Makefile函数

Makefile中的函数众多,可以用来处理变量、文件名等,提供变量的灵活性,返回值可以直接当变量来使用,这里只给出函数调用的一般形式及用法。

语法格式:

$(<function> <arguments>)

语法格式一目了然,函数名与参数列表以空格分开,函数名为make的内置函数,对于自定义的函数需要通过call函数间接调用,参数列表内部的各个变量之间以逗号分隔,如果参数为逗号或空格,不可以直接写出,需要把它们赋值给一个变量再使用,介绍一下subst函数,形式如下:

$(subst <from>,<to>,<text>)

表示在text中,把from全部替换为to,返回新的内容,如下例子:

funcvar := "hello Makefile"
func: echo $(subst l,L,$(funcvar))

输出结果为“heLLo MakefiLe”,小写l全部替换为了大些L,要注意参数列表中的各参数与逗号间不要留有空格,否则替换时空格也起了作用。

下面列举make常用函数及用法。

字符串处理函数——

$(subst FROM,TO,TEXT):字符串替换函数subst,把字符串“TEXT”中的“FROM”字符替换为“TO”,返回替换后的新字符串。

$(patsubst PATTERN,REPLACEMENT,TEXT):模式替换函数patsubst,搜索“TEXT”中以空格分开的单词,将符合模式“PATTERN”的内容替换为“REPLACEMENT”。参数“PATTERN”中可以使用模式通配符“%”来代表一个单词中的若干字符,如果参数“REPLACEMENT”中也包含一个“%”,那么“REPLACEMENT”中的“%”将是“PATTERN”中的那个“%”所代表的字符串,在“PATTERN”和“REPLACEMENT”中,只有第一个“%”被作为模式字符来处理,之后出现的不再作模式字符(作为一个普通字符)。在参数中如果需要将第一个出现的“%”作为字符本身而不作为模式字符时,可使用反斜杠“\”进行转义处理。这个函数返回值为替换后的新字符串。

$(strip STRING):去空格函数strip,去掉字符串(若干单词,使用若干空字符分割)“STRINT”开头和结尾的空字符,并将其中多个连续空字符合并为一个空字符。返回值是无前导和结尾空字符、使用单一空格分割的多单词字符串。

$(findstring FIND,IN):查找字符串函数findstring,在字符串“IN”中查找“FIND”,如果在“IN”之中存在“FIND”,则返回“FIND”,否则返回空。

$(filter PATTERN...,TEXT):过滤函数filter,过滤掉字符串“TEXT”中所有不符合模式“PATTERN”的单词,保留所
有符合此模式的单词。可以使用多个模式,模式中一般需要包含模式字符“%”,存在多个模式时,模式表达式之间使用空格分割。返回值为空格分割的“TEXT”字串中所有符合模式“PATTERN”的字符串。

$(filter-out PATTERN...,TEXT):反过滤函数filter-out与上面的过滤函数filter作用相反,保留不符合模式的部分。

$(sort LIST):排序函数sort,给字符串“LIST”中的单词以首字母为准进行排序(升序),并取掉重复的单词,返回值为空格分割的没有重复单词的字符串。

$(word N,TEXT):取单词函数word,取字符串“TEXT”中第“N”个单词(“N”的值从 1 开始),返回值为字串“TEXT”中第“N”个单词,N为0时出错,N比实际单词个数大时返回空字符串。

$(wordlist S,E,TEXT):取字符串函数wordlist,从字符串“TEXT”中取出从“S”开始到“E”的单词。“S”和“E”
表示单词在字符串中位置的数字。返回字符串“TEXT”中从第“S”到“E”(包括“E”)的单词字符串。

$(words TEXT):统计单词数目函数words,统计字符串“TEXT”中单词的数目,返回值为“TEXT”字符串中的单词数。

$(firstword NAMES...):取首单词函数firstword,取字符串“NAMES…”中的第一个单词,返回值为字符串“NAMES…”的第一个单词。

文件名处理函数——

$(dir NAMES...):取目录函数dir,从文件名序列“NAMES…”中取出各个文件名的目录部分,文件名的目录部分就是包含在文件名中的最后一个斜线(“/”)(包括斜线)之前的部分,返回值为空格分割的文件名序列“NAMES…”中每一个文件的目录部分。

$(notdir NAMES...):取文件函数notdir,从文件名序列“NAMES…”中取出非目录部分,目录部分是指最后一个
斜线(“/”)(包括斜线)之前的部分,删除所有文件名中的目录部分,只保留非目录部分。 返回值为文件名序列“NAMES…”中每一个文件的非目录部分。

$(suffix NAMES...)::取后缀函数suffix,从文件名序列“NAMES…”中取出各个文件名的后缀,后缀是文件名中
最后一个以点“.”开始的(包含点号)部分,如果文件名中不包含一个点号,则为空。返回值为以空格分割的文件名序列“NAMES…”中每一个文件的后缀序列。

$(basename NAMES...):取前缀函数basename,从文件名序列“NAMES…”中取出各个文件名的前缀部分(点号之后的部分),前缀部分指的是文件名中最后一个点号之前的部分,返回值为空格分割的文件名序列“NAMES…”中各个文件的前缀序,如果文件没有前缀,则返回空字串。

$(addsuffix SUFFIX,NAMES...):加后缀函数addsuffix,为“NAMES…”中的每一个文件名添加后缀“SUFFIX”,参数“NAMES…”为空格分割的文件名序列,将“SUFFIX”追加到此序列的每一个文件名的末尾。返回值为以单空格分割的添加了后缀“SUFFIX”的文件名序列。

$(addprefix PREFIX,NAMES...):加前缀函数addprefix,为“NAMES…”中的每一个文件名添加前缀“PREFIX”,参数“NAMES…”是空格分割的文件名序列,将“PREFIX”添加到此序列的每一个文件名之前。返回值为以单空格分割的添加了前缀“PREFIX”的文件名序列。

$(join LIST1,LIST2):单词连接函数join,将字串“LIST1”和字串“LIST2”各单词进行对应连接,就是将“LIST2”
中的第一个单词追加到“LIST1”底第一个单词后合并为一个单词,将“LIST2”中的第二个单词追加到“LIST1”的第二个单词之后合并为一个单词,依次类推。返回值为单空格分割的合并后的单词(文件名)序列。 如果“LIST1”和“LIST2”中的单词数目不一致时,两者中多余部分将作为返回序列的一部分。

$(wildcard PATTERN):获取匹配模式文件名函数wildcard,列出当前目录下所有符合模式“PATTERN”格式的文件名,返回值为空格分割的、存在当前目录下的所有符合模式“PATTERN”的文件名。

其它函数——

$(foreach VAR,LIST,TEXT):如果需要(存在变量或者函数的引用),首先展开变量“VAR”和“LIST”的引用,而表达式“TEXT”中的变量引用不展开。执行时把“LIST”中使用空格分割的单词依次取出赋值给变量“VAR”,然后执行“TEXT”表达式,直到“LIST”的最后一个单词(为空时结束)。“TEXT”中的变量或者函数引用在执行时才被展开,因此如果在“TEXT”中存在对“VAR”的引用,那么“VAR”的值在每一次展开时将会得到不同的值。

$(if CONDITION,THEN_PART[,ELSE_PART]):第一个参数“CONDITION”,在函数执行时忽略其前导和结尾空字
符,如果包含对其他变量或者函数的引用则进行展开。如果“CONDITION”的展开结果非空,则条件为真,就将第二个参数“THEN_PATR”作为函数的计算表达式;“CONDITION”的展开结果为空,将第三个参数“ELSE-PART”作为
函数的表达式,函数的返回结果为有效表达式的计算结果。 返回值为根据条件决定函数的返回值是第一个或者第二个参数表达式的计算结果。当不存在第三个参数“ELSE-PART”,并且“CONDITION”展开为空,函数返回空。

$(call VARIABLE,PARAM,PARAM...):“call”函数是唯一一个可以创建定制化参数函数的引用函数,使用这个函数可以实现对用户自己定义函数的引用,我们可以将一个变量定义为一个复杂的表达式,用“call”函数根据不同的参数对它进行展开来获得不同的结果。在执行时,将它的参数“PARAM”依次赋值给临时变量“$(1)”“$(2)”
(这些临时变量定义在“VARIABLE”的值中)…… call函数对参数的数目没有限制,也可以没有参数值,没有参数值的“call”没有任何实际存在的意义。执行时变量“VARIABLE”被展开为在函数上下文有效的临时变量,变量定义中的“$(1)”作为第一个参数,并将函数参数值中的第一个参数赋值给它;变量中的“$(2)”一样被赋值为函数的第二个参数值;依此类推(变量$(0)代表变量“VARIABLE”本身)。返回值为参数值“PARAM”依次替“$(1)”“$(2)”……之后变量“VARIABLE”定义的表达式的计算值。

$(value VARIABLE):value函数不对变量“VARIBLE”进行任何展开操作,直接返回变量“VARIBALE”的值。这里“VARIABLE”是一个变量名,一般不包含“$”。返回值为变量“VARIBALE”所定义的文本值(如果变量定义为递归展开式,其中包含对其他变量或者函数的引用,那么函数不对这些引用进行展开,函数的返回值包含引用值)。如下面的例子:

FOO = $PATH
all:
@echo $(FOO)
@echo $(value FOO) 

执行make,第一行为“ATH”,这是因为变量“FOO”定义为“$PATH”,所以展开为“ATH”(“$P”为空)。
第二行才是我们需要显示的系统环境变量“PATH”的值(value函数得到变量“FOO”的值为“$PATH”)。

$(eval VARIABLE):函数“eval”是一个比较特殊的函数。使用它可以在Makefile中构造一个可变的规则结构关系(依赖关系链),其中可以使用其它变量和函数。函数“eval”对它的参数进行展开,展开的结果作为Makefile的一部分,make可以对展开内容进行语法解析。展开的结果可以包含一个新变量、目标、隐含规则或者是明确规则等。也就是说此函数的功能主要是根据其参数的关系、结构,对它们进行替换展开。 函数“eval”的返回值时空,也可以说没有返回值。 “eval”函数执行时会对它的参数进行两次展开。第一次展开过程是由函数本身完成的,第二次是函数展开后的结果被作为Makefile内容时由make解析时展开的。实际使用时,如果在函数的展开结果中存在引用,那么在函数的参数中应该使用“$$”来代替“$”,因为这一点,所以通常它的参数中会使用函数“value”来取一个变量的文本值。

$(origin VARIABLE):函数“origin”查询参数“VARIABLE”(一个变量名)的出处。 “VARIABLE”是一个变量名而不是一个变量的引用。因此通常它不包含“$”。返回“VARIABLE”的定义方式,用字符串表示,函数的返回情况有:undefined、default、environment、environment override、file、command line、override以及automatic。

$(shell VARIABLE):执行shell函数。

另外,还有控制make执行的函数,可以输出提示信息,类似于C/C++的assert、log,它们是error、warning,error直接终止程序运行,warning产生警告但不影响运行。

四、条件编译

关键字:ifeq、ifneq、ifdef、ifndef、else、endif。前面两个关键字用于判断相等与否,中间两个变量类似于C/C++中的宏定义,else是个可选的关键字,endif放在条件编译的最后,如下例子:

ifvar :=
ifdef ifvar
ifvar += "defined"
else
ifvar := "not defined"
endif
condition: echo $(ifvar)

“make condition”时,因为ifvar变量为空,make认为是没有定义的,所以输出结果为“not defined”。条件编译各分支中的变量是在make程序解析时立即展开的。ifeq和ifneq的用法有多种格式,如下:

ifeq (VAR1, VAR2)
ifeq 'VAR1' 'VAR2'
ifeq "VAR1" "VAR2"
ifeq 'VAR1' "VAR2"
ifeq "VAR1" 'VAR2'

五、Makefile自动变量

上面的Makefile例子中用到了许多自动变量,下面做个简单的总结。

$@:表示规则的目标文件名。如果目标是一个文档文件(Linux中,一般称“.a”文件为文档文件,也称为静态库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是那个触发规则被执行的目标文件名。

$%:当规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是“foo.a(bar.o)”,那么,这个自动变量的值就为“bar.o”,而“$@”的值为“foo.a”。如果目标不是静态库文件,其值为空。

$<:规则的第一个依赖文件名。如果一个目标文件使用隐式规则来重建,则它代表由隐式规则加入的第一个依赖文件。

$?:所有比目标文件更新的依赖文件列表,空格分割。如果目标是静态库文件名,代表的是库成员(.o文件)。

$^:规则的所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有库成员(.o文件)名。一个文件可重复的出现在目标的依赖中,但只记录它的一次引用情况,会去掉重复的依赖文件。

$+:类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。

$*:当使用了模式匹配即“%”,其值是%代表的值及%之前的部分,如果没有使用模式匹配,最起码文件名要有个后缀,如foo.c,那么这个值为foo,否则为空。

上面的七个自动变量还可以分别加上“D”和“F”,举个例子如下。
$(@D):目标文件集的目录部分,如“dir/foo.o”,其值为“dir”。
$(@F):目标文件集的文件部分,如“dir/foo.o”,其值为“foo.o”。

六、Makefile惯例

1、automake

Makefile文件有一定的书写惯例,工具automake可以帮助我们创建符合这些惯例的Makefile。

2、使用变量代表命令

在Makefile中,一般使用变量来代表对应的命令,例如“MAKE”变量,其值为“make”,这样做的好处是变量比命令本身更加灵活,如果需要修改执行的命令,只需要修改对应的变量的值即可。

3、安装目录

在命令行通过源码编译安装程序时,可通过特定的变量指定安装目录,它们是:prefix、exec_prefix、bindir、sbindir、libexecdir、datadir、sysconfdir、sharedstatedir、localstatedir、libdir、infodir、lispdir、includedir、originincludedir、mandir、man1dir、man2dir、manext、man1ext、man2ext、srcdir,根据这些变量的名字可大概知道它们的作用。

4、标准目标

在使用GNU发布的软件包时,其Makefile包括一些标准的目标,它们是:all、install、uninstall、install-strip、clean、distclean、mostlyclean、maintainer-clean、TAGS、info、dvi、dist、check、installcheck、installdirs,根据这些目标的名字可大概知道它们的作用。

对于install目标,需要将其命令分为三类:正常命令、安装前命令和安装后命令。正常命令是把文件移动到合适的地方,并设置它们的模式,这个过程不修改任何文件,仅仅是把需要安装的文件从软件包中拷贝到安装目录。安装前命令和安装后命令可能修改某些文件,通常,修改一些配置文件和系统的数据库文件,典型地,安装前命令在正常命令之前执行,安装后命令在正常命令执行后执行。

【Bash百宝箱】Makefile快速入门相关推荐

  1. Linux Bash Shell编程快速入门

    BASH 的基本语法 最简单的例子 -- Hello World! 关于输入.输出和错误输出 BASH 中对变量的规定(与 C 语言的异同) BASH 中的基本流程控制语法 函数的使用 2.1     ...

  2. linux 17、 makefile快速入门

    转自:http://blog.csdn.net/liang13664759/article/details/1771246 最近在学习Linux下的C编程,买了一本叫<Linux环境下的C编程指 ...

  3. Linux驱动快速入门

    本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如果错漏,欢迎留言指正 应用层:<LUNIX环境高级编程第二版> <Linux程序设计(第四版)> 内核层:< ...

  4. Apache Hive 快速入门 (CentOS 7.3 + Hadoop-2.8 + Hive-2.1.1)

    2019独角兽企业重金招聘Python工程师标准>>> 本文节选自<Netkiller Database 手札> 第 63 章 Apache Hive 目录 63.1. ...

  5. git 上传项目到linux仓库_「成都校区」Git使用快速入门

    Git使用快速入门 1.在Linux上安装Git 输入git命令,查询是否安装了git (1)如果是Debian或Ubuntu Linux系统,使用如下命令进行安装: apt-get install ...

  6. 3、kubernetes应用快速入门190625

    一.kubernetes应用入门 1.kubectl命令 Basic Commands create Create a resource from a file or from stdin.expos ...

  7. python快速编程入门黑马-新手如何快速入门Python编程?/开发python入门教程

    Python从入门到实践的教程是什么? 第一阶段Python与Linux数据这是Python的入段,也是帮助零基础学员打好基础的重要阶段,让零基础学员可以具备基础的编程能力,并掌握MySQL进阶内容. ...

  8. Python/WSGI 应用快速入门--转

    http://uwsgi-docs-cn.readthedocs.io/zh_CN/latest/WSGIquickstart.html 这个快速入门指南将会向你展示如何部署简单的 WSGI 应用和普 ...

  9. .NET Core实战项目之CMS 第五章 入门篇-Dapper的快速入门看这篇就够了

    写在前面 上篇文章我们讲了如在在实际项目开发中使用Git来进行代码的版本控制,当然介绍的都是比较常用的功能.今天我再带着大家一起熟悉下一个ORM框架Dapper,实例代码的演示编写完成后我会通过Git ...

最新文章

  1. 中大型网站静态资源优化及存储
  2. HTML5 canvas drawImage() 方法记录
  3. web怎么用代码创造表格_Python新工具:用三行代码提取PDF表格数据
  4. linux Hadoop HDFS NameNode进程重启脚本(linux进程通用)
  5. Python3 安装PyMySQL
  6. Qt编写自定义控件及插件的使用
  7. C/C++报错:全局变量重定义或是多次定义
  8. 天庭最牛系统 推荐下载_PPT中有哪些特别好用的插件?(含下载链接)
  9. Selenium2+python自动化8-SeleniumBuilder辅助定位元素
  10. Python_遍历时删除的处理说明
  11. Rootkit检测技术发展现状
  12. 国家计算机考试一级难不难,国家计算机一级考试难不难?
  13. 解决Tomcat在cmd乱码问题
  14. TCPMP-interface相关文件函数解析-Mediainfo.c
  15. 《高效能人士的7个习惯》PDF,复习笔记(上)
  16. Springboot Web应用中服务器配置参数ServerProperties的加载
  17. 怎么截取巨大日志里某一天的log
  18. webrtc服务器开发--搭建环境
  19. html界面——button设置样式
  20. rangeOfString用法

热门文章

  1. “诱饵效应”让用户产生“冲突”营销心理学十大效应 营销值得学
  2. HTML——简单的网站首页
  3. 网页制作语言HTML5的应用
  4. 批量解压多个rar压缩包并将解压出来的文件以该压缩包的名称重命名
  5. guzzle/guzzle 日常使用
  6. java8新特性之stream
  7. 指纹识别综述(11): 专利
  8. Exp9 Web安全基础 20164302 王一帆
  9. Frenet坐标系与Cartesian坐标系互转(三):应用示例
  10. ROS moveit cartesian_demo 机械臂笛卡尔空间路径速度限制