makefile 使用指南

跟我一起写Makefile详细原版一https://seisman.github.io/how-to-write-makefile/overview.html
跟我一起写Makefile详细原版二https://blog.csdn.net/haoel/article/details/2887
https://blog.csdn.net/haoel/article/details/2886

makefile语法规则
1.规则举例

 foo.o: foo.c defs.h       # foo模块
           cc -c -g foo.c

看到这个例子,各位应该不是很陌生了,前面也已说过,foo.o是我们的目标,foo.c和defs.h是目标所依赖的源文件,而只有一个命令“cc -c -g foo.c”(以Tab键开头)。这个规则告诉我们两件事:
1. 文件的依赖关系,foo.o依赖于foo.c和defs.h的文件,如果foo.c和defs.h的文件日期要比foo.o文件日期要新,或是foo.o不存在,那么依赖关系发生。
2. 如果生成(或更新)foo.o文件。也就是那个cc命令,其说明了,如何生成foo.o这个文件。(当然foo.c文件include了defs.h文件)
2 规则的语法

targets : prerequisites
       command

或是这样:

targets : prerequisites ; command
           command

makefile 核心公式

同时上一个 公式中的 源文件 又可以嵌套成为下一个公式的 目标文件。周而复始。

targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以[Tab键]开头,如果和prerequisites在一行,那么可以用分号做为分隔。(见上)
prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。这个在前面已经讲过了。
如果命令太长,你可以使用反斜框(‘\’)作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事,文件的依赖关系和如何成成目标文件。
一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。

1、make 如何工作
默认方式

直接输入make,则
**1.**make会在当前的目录下找到名为“Makefile”或者“makefile”的文件。
**2.**如果找到,它会把文件中第一个target作为最终的目标文件(如上面例子中的count_words)。
1.首先,make会检查目标count_words的prerequisite文件count_words.o, lexer.o 和 -lfl。
2.count_words.o通过编译count_words.c生成
3.lexer.o通过编译lexer.c 生成,但是lexer.c 并不存在,因此会继续寻找lexer.c的生成方式,并找到了通过flex程序将 lexer.l生成为lexer.c。
4.最后,make会检查-lfl,-l是gcc的一个命令选项,表示将系统库链接到程序。而"fl"对应的是libfl.a的库。(GNU make 可以识别这样的命令,当一个prerequisite是以这种-l的形式表示出来的时候,make会自己搜索lib.so的库文件,如果没找到则继续搜索lib.a的库文件)。这里make找到的是/usr/lib/libfl.a文件,并将它与程序进行连接。
**3.**如果count_words文件不存在,或者count_words所依赖的后面的.o文件的修改时间比count_words本身更加新,那么,它会执行后面定义的命令来生成这个count_words文件。如果count_words所依赖的.o文件也不存在,那么make会继续按照前面的方式生成.o文件。
**4.**找到相应的.c和.h,用来生成.o,然后再用.o完成make的最终任务。

关于Clean
一个好习惯是每个makefile都要写clean规则
.PHONY表示clean是一个“伪目标”,而rm命令前面的减号则表示,不管出现什么问题都要继续做后面的事情。
注意:clean规则不要放在makefile的开头,不然就会变成make的默认目标了。
伪目标也可也作为makefile的默认目标,放在文件的最前端,由于伪目标的特性,他指出的所有prerequisite都会被重新编译。这样可以用来同时生成多个目标。另外,与普通目标文件一样,伪目标也可也使用依赖关系,例如:

.PHONY: clean cleano cleanc  
clean: cleano cleanc  
        -rm $(program)  
cleano:   
        -rm *.o  
cleanc:  
        -rm lexer.c  

在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

一、显式规则(Explicit Rules)
通配符(Wildcards)

make支持的通配符与Bourne shell基本相同,包括~, *, ?, […], [^…]。
. 表示了所有文件;
? 表示任意单个字符;
[…] 表示一个字符类;
[^…] 表示相反的字符类。
~ 表示当前用户的/home路径,~加用户名可以表示该用户的/home路径。
注意 : make会在读取makefile的时候就自动补充好通配符替代的内容,而shell则是在执行命令的时候才会进行通配符替代,

在某些复杂情况,这两种方式会有极大的区别。
空目标(Empty Targets)
空目标是伪目标的一种变形形式,通常情况下通过创建一个空文件来实现。例如:

size: count_words.o  
        size $^  
        touch size

二、变量
变量最简单的形式就是:
( v a r i a b l e n a m e ) ∗ ∗ 变 量 ( = 替 换 ) ( + = 追 加 ) ( : = 常 量 ) ( (variable_name) **变量(=替换)(+=追加)(:=常量)( (variablename)=+==(变量名)替换)**
变量可以包含几乎所有的字符包括标点符号。一般情况下,变量名需要被$( )所包裹,但是当变量名只有一个字符时,括号可以省略。makefile可以定义很多变量,但同时make本身也定义了一些自动变量。

-------------------------------------------------------------------------------------

自动变量是make自动根据规则生成的,不需要用户显式的指出相应的文件或目标名称。以下就是七个最核心的自动变量:
$@ 目标文件的文件名;
$% 仅当目标文件为归档成员文件(.lib 或者 .a)时,显示文件名,否则为空;
$< 依赖(prerequisite)列表里面的第一个文件名;
$? 所有在prerequisite列表里面比当前目标新的文件名,用空格隔开;
$^ 所有在prerequisite列表中的文件,用空格隔开; 如果有重复的文件名(包含扩展名),会自动去除重复;
+ 与 + 与 +^相似,也是prerequisite列表中的文件名用空格隔开,不同的是这里包含了所有重复的文件名;
∗ 显 示 目 标 文 件 的 主 干 文 件 名 , 不 包 含 后 缀 部 分 。 此 外 , 上 面 的 每 个 变 量 都 带 有 两 个 不 同 的 变 种 , 用 于 适 应 不 同 种 类 的 m a k e 。 分 别 是 在 后 面 附 加 一 个 “ D ” 或 者 “ F ” 。 例 如 , * 显示目标文件的主干文件名,不包含后缀部分。 此外,上面的每个变量都带有两个不同的变种,用于适应不同种类的make。分别是在后面附加一个“D”或者“F”。例如, makeDF(^D)就是代表所有依赖文件的路径,$(<F)表示依赖文件第一个的文件部分的值。

-------------------------------------------------------------------------------------

CC = gcc  
object = lexer.o count_words.o  
program = count_words 
#目标文件:依赖文件
#	(命令) 
$(program): size $(object) -lfl  
        $(CC) $(object) -lfl -o $@  
count_words.o: count_words.c  
        $(CC) -c $^  
lexer.o: lexer.c  
        $(CC) -c $^  
lexer.c: lexer.l  
        flex -t $^ > $@   
size: count_words.o  
        size $^  
        touch size  
.PHONY: clean cleano cleanc  
clean: cleano cleanc  
        -rm $(program)  
cleano:   
        -rm *.o  
cleanc:  
        -rm lexer.c  

-------------------------------------------------------------------------------------

#对应关系 在本makefile中以空格隔开的后缀为.c 都会为其生成一个新的.d文件 意图为更新所有*.c文件的include依赖关系
  %.d : %.c
          @echo 'finding $< depending head file'
          @$(CC) -MT"$(<:.c=.o) $@" -MM $(INCLUDE_PATH) $(CPPFLAGS) $< > $@
#对于include中的*.d文件,只要里面任意有一个文件被修改,那么就会触发此规则生成一个新的*.o文件
%.o: %.d
        @echo compile $(<:d=c)
        @$(CC) -c $(<:.d=.c) $(INCLUDE_PATH) $(CFLAGS) $@
 
  sinclude $(Sources:.c=.d)
 $(Bin) : $(OBJS)
        @echo bulding....
        @$(CC) $(OBJS)  $(CFLAGS) $(Bin)
        @echo created file: $(BinName)
    #########################################################################
 #    单独的 < 符号代表 依存源文件(即冒号: 的左边) $< 代表将源文件展开成为字符
 #    单独的 @ 符号代表 目标文件   (冒号 : 的右边)  $@ 代表将目标文件名称展开成为字符
 #    符号 @ 后接命令则表示:此语句执行,但并不现实 
 #    例如:@$(CC) $(OBJS)  $(CFLAGS) $(Bin)   
 #    只执行链接命令,但是不将此字符串打印至终端
 #    关键字:@echo  表示该行后的命令只显示 不执行。 
 #    注意:虽然只显示,但是他依旧会以执行命令的要求的解析文本,
 #    只是不执行而已,如果需要输出字符串使用‘  ’将内容引用即可
 #    重点符号 $ : 表示转义,在makefile中无论在哪里都会被识别为转义字符,
 #    如果想表示 $符号,那么需要使用 $$ 
 #    例如:@echo ‘$$$$’  终端将会输入 : "$$" 
 #    其余makefile 知识参考  《跟我一起写 MakeFile》 ----陈皓  
 #    #########################################################################

大于号>是shell的重定向

generate text.g -big 这样会输出一些信息到屏幕(如果确实有这么个generate命令)
generate text.g -big > bigoutput 这样会创建一个文件bigoutput,然后把本来应该输出屏幕的信息写到这个文件里。
另一个重定向的例子:ls会显示文件列表,如果用 ls > lsoutput,文件列表就不会显示到屏幕而是写入了lsoutput文件,cat lsoutput 或者 vi lsoutput就可以看到结果。
比如,假设文件 a.txt 依赖于 b.txt 和 c.txt ,是后面两个文件连接(cat命令)的产物。那么,make 需要知道下面的规则
a.txt: b.txt c.txt cat b.txt c.txt > a.txt

-------------------------------------------------------------------------------------

变量的高级用法
变量的替换引用
我们定义变量的目的是为了简化我们的书写格式,代替我们在代码中频繁出现且冗杂的部分。它可以出现在我们规则的目标中,也可以是我们规则的依赖中。我们使用的时候会经常的对它的值(表示的字符串)进行操作。遇到这样的问题我们可能会想到我们的字符串操作函数,比如 “patsubst” 就是我们经常使用的。但是我们使用变量同样可以解决这样的问题,我们通过下面的例子来具体的分析一下。
实例:

foo:=a.c b.c d.c
obj:=$(foo:.c=.o)
All:
    @echo $(obj)

这段代码实现的功能是字符串的后缀名的替换,把变量 foo 中所有的以 .c 结尾的字符串全部替换成 .o 结尾的字符串。我们在 Makefile 中这样写,然后再 shell 命令行执行 make 命令,就可以看到打印出来的是 “a.o b.o d.o” ,实现了文件名后缀的替换。
注意:括号中的变量使用的是变量名而不是变量名的引用,变量名的后面要使用冒号和参数选项分开,表达式中间不能使用空格。第二个变量 obj 是对整体的引用。
上面的例子我们可以换一种更加通用的方式来写,代码展示如下:

foo:=a.c b.c d.c
obj:=$(foo:%.c=%.o)
All:
    @echo $(obj)

我们在 shell 中执行 make 命令,发现结果是相同的。
对比上面的实例我们可以看到,表达式中使用了 “%” 这个字符,这个字符的含义就是自动匹配一个或多个字符。在开发的过程中,我们通常会使用这种方式来进行变量替换引用的操作。
为什么这种方式比第一种方式更加实用呢?我们在实际使用的过程中,我们对变量值的操作不只是修改其中的一个部分,甚至是改变其中的多个,那么第一种方式就不能实现了。我们来看一下这种情况:

foo:=a123c a1234c a12345c
obj:=$(foo:a%c=x%y)
All:
    @echo $(obj)

我们可以看到这个例子中我们操作的是两个不连续的部分,我们执行 make 后打印的值是 “x123y x1234y x12345y”,这种情况下我们使用第一种情况就不能实现,所以第二种的使用更全面。
变量的嵌套使用
变量的嵌套引用的具体含义是这样的,我们可以在一个变量的赋值中引用其他的变量,并且引用变量的数量和和次数是不限制的。下面我们通过几个实例来说明一下。
实例 1:

foo:=test
var:=$(foo)
All:
    @echo $(var)

这种用法是最常见的使用方法,打印出 var 的值就是 test。我们可以认为是一层的嵌套引用。
实例 2:

foo=bar
var=test
var:=$($(foo))
All:
    @echo $(var)

我们再去执行 make 命令的时候得到的结果也是 test,我们可以来分析一下这段代码执行的过程:$(foo) 代表的字符串是 bar,我们也定义了变量 bar,所以我们可以对 bar 进行引用,变量 bar 表示的值是 test,所以对 bar 的引用就是 test,所以最终 var 的值就是 test。这是变量的二层嵌套执行,当然我们还可以使用三层的嵌套执行,写法跟上面的方式是一样的。嵌套的层数也可以更多,但是不提倡使用。
我们再去使用变量的时候,我们并不是只能引用一个变量,可以有多个变量的引用,还可以包含很多的变量还可以是一些文本字符。我们可以通过一些例子来说明一下。
实例 4:

first_pass=hello
bar=first
var:=$(bar)_pass
all:
    @echo $(var)

在命令行执行 make 我们可以得到 var 的值是 hello。这是变量嵌套引用的时候可以包含其它字符的使用情况。
实例 5:

first_pass=hello
bar=first
foo=pass
var:=$(bar)_$(foo)
all:
    @echo $(var)

这个实例跟上面实例的运行结果是一样的。我们可以看到这个实例中使用了两个变量的引用还有其它的字符。
变量的嵌套引用和我们的变量的递归赋值的区别:嵌套引用的使用方法就是用一个变量表示另外一个变量,然后进行多层的引用。而递归展开的变量表示当一个变量存在对其它变量的引用时,对这变量替换的方式。递归展开在另外一个角度描述了这个变量在定义是赋予它的一个属性或者风格。并且我们可以在定义个一个递归展开式的变量时使用套嵌引用的方式,但是建议你的实际编写 Makefile 时要尽量避免这种复杂的用法。
在实际使用的过程中变量的第一种用法经常使用的,第二种用法我们很少使用,应该说是尽量避免使用变量的嵌套引用。在必须要使用的时候我们应该做到嵌套的层数是越少越好的。因为使用这种方法表达会比较的复杂,如果条理不清楚的话我们就会出错。并且在给其他人看的时候也会不容易理解。

三、 查找文件(VPATH)
为了让make能够找到相应的位置,需要在makefile开头添加VPATH参数,显式的指出源文件和头文件的路径:

VPATH = src include 

此外,不仅make需要知道路径,gcc同样需要,通过添加编译选项 -I 的方式,显式的告诉gcc头文件的位置:

CPPFLAGS = -I include

最终,makefile为:

VPATH=src include  
CC = gcc  
CPPFLAGS = -I include  
count_words: count_words.o counter.o lexer.o -lfl  
        $(CC) $^ -o $@  
count_words.o: count_words.c counter.h  
        $(CC) $(CPPFLAGS) -c $<   
counter.o: counter.c counter.h lexer.h  
        $(CC) $(CPPFLAGS) -c $<  
lexer.o: lexer.c include/lexer.h  
        $(CC) $(CPPFLAGS) -c $<  
lexer.c: lexer.l  
        flex -t $< > $@  
.PHONY: clean  
clean:   
        rm *.o lexer.c count_words 

注意1: VPATH变量可以包含一个路径列表,当make需要一个文件时会在其中搜索。这个列表既可以作为目标文件也可作为关联文件的路径,但不能作为下面命令行程序中文件的路径。这正是为什么在命令行程序中使用自动化变量的原因,避免因为路径修改而导致的命令运行错误。
注意2: 如果是因为make的相关路径配置错误,终端会输出例如:

make: No rule to make target `count_words.c', needed by `count_words.o'. Stop.

但如果是因为gcc的头文件路径配置错误,在终端会提示,例如:

src/counter.c:1:19: fatal error: lexer.h: No such file or directory
compilation terminated.

注意3: 在UNIX系统中,路径列表可以被空格或者冒号分隔开,在Windows中则是用空格或者分号。(既然两种系统都用空格,那最好就使用空格)
注意4: make会在每次需要文件的时候搜索VPATH列表中的路径,如果有两个不同路径下文件重名,则make只会使用顺序查找到的第一个。
更加准确的方式是使用 vpath 变量,它的语法是:

vpath pattern directory-list

因此,上面makefile中的VPATH可以写做:

vpath %.c src
vpath %.l src
vpath %.h include

这样就告诉了make去src/中寻找.c和.l文件,去include中寻找.h文件。

四、 模式匹配规则
通常情况下,编译器会将带有它可以识别后缀名的文件编译成相应的目标文件。例如,C语言的编译器会将.c后缀名的文件编译成带有.o后缀名的目标文件。再比如,前面的用到过的flex使用.l后缀名文件作为输入,输出则是.c的文件。事实上,这样一些约定可以根据文件名模式,通过内建规则来进行处理。例如,用内建规则,之前的makefile可以简写做:

VPATH=src include  
CC = gcc  
CPPFLAGS = -I include  
  
count_words: counter.o lexer.o -lfl  
count_words.o: counter.h  
counter.o: counter.h lexer.h  
lexer.o: lexer.h  
.PHONY: clean  
clean:   
        rm *.o lexer.c count_words  

所有的内建规则都是模式匹配规则的实例,这个makefile之所以可以使用,是因为三个内建规则。
规则一: 从.c到.o

#(%.c %.o任意的.c或.o)
%.o: %.c  
        $(COMPILE.c) $(OUTPUT_OPTION) $<  

规则二: 从.l 到.c

%.c: %.l  
        @$(RM) $@  
        $(LEX.l) $< > $@  

规则三: 从.c到无后缀名
当生成目标没有后缀名的时候(通常是可执行文件)

%%.c  
        $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ 

依照上述的模式匹配规则,make的生成过程如下:

gcc  -I include   -c -o count_words.o src/count_words.c
gcc  -I include   -c -o counter.o src/counter.c
lex  -t src/lexer.l > lexer.c
gcc  -I include   -c -o lexer.o lexer.c
gcc   count_words.o counter.o lexer.o /usr/lib/x86_64-linux-gnu/libfl.so   -o count_words
rm lexer.c

STEP 1: make根据makefile中的内容,将默认目标设置为count_words(如果命令行中特别指出,则为其它,如clean)。根据依赖关系,分别是count_words.o(虽然没有在makefile显式的指出,但make会根据隐式规则自动填充), counter.o, lexer.o 和 -lfl。
**STEP 2:**根据依赖关系列表中的顺序,make会先找到count_words.o,由于count_words.o的依赖关系没有后续更新,因此make只需要找到count_word.c并进行编译。在当前目录下,没有count_word.c的情况下,make会根据VPATH变量继续寻找,直到在src/中找到。接下来,counter.o的编译过程也是一样的。
STEP3: 编译lexer.o的过程比前面多了一步:因为工程中并不存在lexer.c,于是make发现了从lexer.l生成lexer.c的模式匹配规则。
STEP4: make检查-lfl库的具体位置,本人用的是Ubuntu12.04 64bit, 因此对应的路径为: /usr/lib/x86_64-Linux-gnu/libfl.so,这个路径跟操作系统和make的版本有关,其实它具体在哪都不影响make的编译(只要是make可以找到的地方)。
STEP5: make已经准备好了生成count_words所需的所有依赖文件,生成。
**STEP6:**注意到,make创建的lexer.c是一个中间文件,makefile中并没有要生成它,因此在编译完成后将它删除。
DONE!

事实上,每一个makefile都有一个专有的内置规则库,在相应目录下可以使用下面的命令查看这个库(注意内容偏多,可以用more来分开看,或者重定向输出到文件)

make --print-data-base 

模式匹配
模式匹配规则中使用的百分号“%”与UNIX shell里面的通配符 “*”非常类似,它也可以代表任何长度的字符,并能被放在模式匹配中的任何位置,但在一个模式匹配中 只能出现一次。
下面这些例子都是合法的模式匹配:

%,v
s%.o
wrapper_%

静态模式规则
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:

<targets...>: <target-pattern>: <prereq-patterns ...>
   <commands>

targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern是指明了targets的模式,也就是的目标集模式。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的定义成“%.o”,意思是我们的集合中都是以“.o”结尾的,而如果我们的定义成“%.c”,意思是对所形成的目标集进行二次定义,其计算方法是,取模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。
所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
看一个例子:

 objects = foo.o bar.o
        all: $(objects)
        $(objects): %.o: %.c
        $(CC) -c $(CFLAGS) $< -o $@

上面的例子中,指明了我们的目标从 o b j e c t 中 获 取 , “ object中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量 objectobject集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“foobar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.cbar.c”。而命令中的“ < ” 和 “ <”和“ <@”则是自动化变量,“ < ” 表 示 所 有 的 依 赖 目 标 集 ( 也 就 是 “ f o o . c b a r . c ” ) , “ <”表示所有的依赖目标集(也就是“foo.c bar.c”),“ <foo.cbar.c@”表示目标集(也褪恰癴oo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:

 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”有几百个,那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会一个很强大的功能。再看一个例子:

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 $<

( f i l t e r (filter%.o, (filter(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了Makefile中更大的弹性。

五、 隐含规则数据库
GNU make 3.80拥有90多个内建隐含规则。隐含规则即是模式匹配规则又是后缀规则。这些规则支持的语言有很多: C++, Pascal, FORTRAN, ratfor, Modula, Texinfo, TEX (包括Tangle 和 Weave), Emacs Lisp, RCS, SCCS等。但如果你想要编译JAVA或者XML,你可以自己编写规则。(别担心,事实上它们非常简单)
你可以通过–print-data-base或者-p参数来查看make的内建规则数据库(小心,输出有n多行)。
使用隐含规则
当处理一个目标文件没有发现显式规则时,make就会调用隐含规则。其实,只要不在makefile中目标文件的命令行程序部分添加任何内容,就可以调用隐含规则。
这种方式通常很好,在极特殊的情况下会导致一些问题。例如,在混编过程中使用Lisp和C两种语言,同一个路径下分别有editor.l和editor.c两个文件,使用make隐含规则编译的时候,make有可能将editor.l认做flex的文件,并将它编译成editor.c(正如前面(上)部分的例子)。于是,真正的editor.c就会被覆盖掉。要想避免这个问题,就需要将flex编译相关的两个内建规则删掉:

%.o: %.l  
%.c: %.l  

这样的模式规则不带有任何的命令,就可以将他们从make的数据库删除。尽管在实际操作中,这种规则导致的错误非常罕见,但是知道有这样一种情况总是会在不经意的时候对你有所帮助。
make的另一个强大之处在于,对于每一个符合模式匹配的目标文件,make会为它寻找相应的依附条件。如果找到了符合依附条件模式的源文件,这条规则才会生效。但当找不到时,make会再次查找所有的规则,并假设符合依附关系的源文件是另外的一个需要被生成的目标文件。这样,make会递归式的找到一个规则链用以更新目标文件(就像前面的例子一样,make可以根据规则链从lexer.l生成到lexer.o)。
例如一个名为a.o的文件的源文件可能是.c,.cpp,.cc,.p,.f,.r,.s,.mod等等。

规则结构
为了方便用户自定义,内建规则库都有标准的结构。以从C程序生成目标文件的规则为例:

%.o: %.c  
        $(COMPILE.c) $(OUTPUT_OPTION) $<

用户自定义的部分完全取决于变量的使用,事实上这两个变量也是由其他多个变量和参数决定的:

COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c  
CC = gcc  
OUTPUT_OPTION = -o $@  

需要注意的是,在makefile中设置参数时需要避免将这些变量赋值,如果在makefile中设置:

CPPFLAGS = -I include/

那么,当需要在生成过程中加入命令行参数

make CPPFLAGS=-DDEBUG

则-I选项和它的参数就会被取消掉。因为在命令行里面的变量将重写其他所有对变量的设置。因此,这样的设置将最终导致make找不到头文件的位置,而造成编译失败。

帮助命令
大型的makefile会包含大量的目标文件,并且非常不容易被记住,一个简单的解决方式就是为默认的目标文件设置帮助命令,然后手工方式维护这些命令又是相当复杂和繁琐的。因此,make的规则数据库提供了命令用于直接使用,下面的例子是使用了这些命令按顺序输出了所有目标列表(每行四个):

.PHONY: help  
help:  
        make --print-data-base --question |             \  
        awk '/^[^.%][-A-Za-z0-9_]*:/                    \  
        { print substr(
1)-1) }' |      \  
        sort |                                          \  
        pr --omit-pagination --width=80 --columns=4   

执行make help后就会在屏幕上看到所有的目标文件。
简单解释一下这个命令: 首先,使用–print-data-base查找出规则数据库的内容;然后使用awk命令从内容中抓取到目标文件信息,去掉以百分号和点号开头的文件(模式匹配规则和后缀规则文件),并删掉这一行多余的内容;最后将列表排序并按四个一行输出到屏幕。

六、 特殊目标文件
特殊目标文件是一种改变make默认方式的内建伪目标。例如,.PHONY会声明一个文件不会依赖任何其他真实的文件,并且永远都需要更新。伪文件.PHONY是最常见的特殊目标文件,但是还有些其他特殊文件。特殊文件也遵循着target: prerequisite的语法规则,但目标文件并不是一个文件,他们更像是修改make内部算法的指令。
特殊文件共有十二个,分为三类:一类是为了改变make在更新目标时的动作;还有一类是作为全局标志的形式,编译或忽略他们的目标文件;最后一类是后缀名特殊目标,当指明了旧的后缀规则时使用。
最常用的目标修饰符有:
.INTERMEDIATE
这个特殊目标文件的依赖关系被视为中间文件,当make更新其他文件时创建了列表中的文,make会在结束时删除这些文件;但如果更新前这个文件已经存在,则make不会删除它。
.SECONDARY
依赖列表中的文件会被当作中间文件,但不会被自动删除。这个特殊目标最常见的地方是针对一些库文件,为了方便调试过程,开发期间使用的库文件尽管也是中间文件,但保留着它可以减少调试中的重复编译过程。
.PRECIOUS
当make在执行过程中被中断时,它会将所有这次更新过的目标文件删除。因此,make不会将半成品文件遗留在编译路径中。但是,当某些生成的文件相当大或者运算非常费时的结果。因此,如果将这类文件定义为PRECIOUS,则它们就不会在中断时被删除掉了。尽管.PRECIOUS不太常见,但是它经常会在需要的时候起到意想不到的效果。 注意:make不会在发生错误时自动删除文件,只有当它被信号中断时才会。
.DELETE_ON_ERROR
这个正好和.PRECIOUS相反,它会使依赖关系列表中的文件在发生错误时被删除。

七、 自动生成依赖关系
在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句“#include “defs.h””,那么我们的依赖关系应该是:

 main.o : main.c defs.h

但是,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,如果我们执行下面的命令:

cc -M main.c

其输出是:

 main.o : main.c defs.h

于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
gcc-M main.c的输出是:

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的输出则是:

main.o: main.c defs.h

那么,编译器的这个功能如何与我们的Makefile联系在一起呢。因为这样一来,我们的Makefile也要根据这些源文件重新生成,让Makefile自已依赖于源文件?这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。
于是,我们可以写出[.c]文件和[.d]文件的依赖关系,并让make自动更新或自成[.d]文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。
这里,我们给出了一个模式规则来产生[.d]文件:

 %.d: %.c
           @set -e; rm -f $@; \
            $(CC) -M $(CPPFLAGS) $< > $@.; \
            sed 's,\.o[ :]*,\1.o $@ : ,g' < $@. > $@; \
            rm -f $@.

这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm-f @ ” 的 意 思 是 删 除 所 有 的 目 标 , 也 就 是 [ . d ] 文 件 , 第 二 行 的 意 思 是 , 为 每 个 依 赖 文 件 “ @”的意思是删除所有的目标,也就是[.d]文件,第二行的意思是,为每个依赖文件“ @[.d]<”,也就是[.c]文件生成依赖文件,“$@”表示模式“%.d”文件,如果有一个C文件是name.c,那么“%”就是“name”,“”意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。
总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
于是,我们的[.d]文件也会自动更新了,并会自动生成了,当然,你还可以在这个[.d]文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用Makefile的“include”命令,来引入别的Makefile文件(前面讲过),例如:
sources = foo.c bar.c
include ( s o u r c e s : . c = . d ) 上 述 语 句 中 的 “ (sources:.c=.d) 上述语句中的“ (sources:.c=.d)(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变量$(sources)所有[.c]的字串都替换成[.d],关于这个“替换”的内容,在后面我会有更为详细的讲述。当然,你得注意次序,因为include是按次来载入文件,最先载入的[.d]文件中的目标会成为默认目标

显示命令
通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:

@echo 正在编译XXX模块......

当make执行时,会输出“正在编译XXX模块…”字串,但不会输出命令,如果没有“@”,那么,make将输出:

   echo 正在编译XXX模块......
   正在编译XXX模块......

如果make执行时,带入make参数“-n”或“–just-print”,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
而make参数“-s”或“–slient”则是全面禁止命令的显示。

命令执行
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:示例一:

   exec:
           cd /home/hchen
           pwd

示例二:

   exec:
           cd /home/hchen; pwd

当我们执行“make exec”时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。
make一般是使用环境变量SHELL中所定义的系统Shell来执行命令,默认情况下使用UNIX的标准Shell——/bin/sh来执行命令。但在MS-DOS下有点特殊,因为MS-DOS下没有SHELL环境变量,当然你也可以指定。如果你指定了UNIX风格的目录形式,首先,make会在SHELL所指定的路径中找寻命令解释器,如果找不到,其会在当前盘符中的当前目录中寻找,如果再找不到,其会在PATH环境变量中所定义的所有路径中寻找。MS-DOS中,如果你定义的命令解释器没有找到,其会给你的命令解释器加上诸如“.exe”、“.com”、“.bat”、“.sh”等后缀。

八.常用的函数
2.1foreach

$(foreach var,list,text)

简单地说,就是 for each var in list, change it to text。
对list中的每一个元素,取出来赋给var,然后把var改为text所描述的形式。
例子:

objs := a.o b.o
dep_files := $(foreach  f,  $(objs),  .$(f).d)  // 最终 dep_files := .a.o.d .b.o.d

2.2wildcard

$(wildcard pattern)

pattern所列出的文件是否存在,把存在的文件都列出来。
例子:

src_files := $( wildcard  *.c)  // 最终 src_files中列出了当前目录下的所有.c文件

2.3filter

$(filter pattern...,text)

把text中符合pattern格式的内容,filter(过滤)出来、留下来。
例子:

obj-y := a.o b.o c/ d/
DIR :=  $(filter  %/,  $(obj-y))   //结果为:c/ d/

2.4filter-out
把text中符合pattern格式的内容,filter-out(过滤)出来、扔掉。
例子:

obj-y := a.o b.o c/ d/
DIR :=  $(filter-out  %/,  $(obj-y))   //结果为:a.o  b.o

2.5patsubst
寻找text’中符合格式pattern’的字,用replacement’替换它们。pattern’和`replacement’中可以使用通配符。
例子:

subdir-y    :=  c/  d/
subdir-y    := $(patsubst  %/,  %,  $(subdir-y))   // 结果为:c  d

**使用条件判断**
使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。
一、示例
下面的例子,判断$(CC)变量是否“gcc”,如果是的话,则使用GNU函数编译目标。
```powershell
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif

可见,在上面示例的这个规则中,目标“foo”可以根据变量“ ( C C ) ” 值 来 选 取 不 同 的 函 数 库 来 编 译 程 序 。 我 们 可 以 从 上 面 的 示 例 中 看 到 三 个 关 键 字 : i f e q 、 e l s e 和 e n d i f 。 i f e q 的 意 思 表 示 条 件 语 句 的 开 始 , 并 指 定 一 个 条 件 表 达 式 , 表 达 式 包 含 两 个 参 数 , 以 逗 号 分 隔 , 表 达 式 以 圆 括 号 括 起 。 e l s e 表 示 条 件 表 达 式 为 假 的 情 况 。 e n d i f 表 示 一 个 条 件 语 句 的 结 束 , 任 何 一 个 条 件 表 达 式 都 应 该 以 e n d i f 结 束 。 当 我 们 的 变 量 (CC)”值来选取不同的函数库来编译程序。 我们可以从上面的示例中看到三个关键字:ifeq、else和endif。ifeq的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。else表示条件表达式为假的情况。endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。 当我们的变量 (CC)ifeqelseendififeqelseendifendif(CC)值是“gcc”时,目标foo的规则是:

foo: $(objects)
$(CC) -o foo $(objects) $(libs_for_gcc)

而当我们的变量$(CC)值不是“gcc”时(比如“cc”),目标foo的规则是:

foo: $(objects)
$(CC) -o foo $(objects) $(normal_libs)

当然,我们还可以把上面的那个例子写得更简洁一些:

libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)

二、语法
条件表达式的语法为:

<conditional-directive>
<text-if-true>
endif

以及:

<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

其中表示条件关键字,如“ifeq”。这个关键字有四个。
第一个是我们前面所见过的“ifeq”

ifeq (<arg1>, <arg2> )
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"

比较参数“arg1”和“arg2”的值是否相同。当然,参数中我们还可以使用make的函数。如:

ifeq ($(strip $(foo)),)
<text-if-empty>
endif

这个示例中使用了“strip”函数,如果这个函数的返回值是空(Empty),那么就生效。
第二个条件关键字是“ifneq”。语法是:

ifneq (<arg1>, <arg2> )
ifneq '<arg1>' '<arg2>'
ifneq "<arg1>" "<arg2>"
ifneq "<arg1>" '<arg2>'
ifneq '<arg1>' "<arg2>"

其比较参数“arg1”和“arg2”的值是否相同,如果不同,则为真。和“ifeq”类似。
第三个条件关键字是“ifdef”。语法是:
ifdef
如果变量的值非空,那到表达式为真。否则,表达式为假。当然,同样可以是一个函数的返回值。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。还是来看两个例子:
示例一:

bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif

示例二:

foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif

第一个例子中,“$(frobozz)”值是“yes”,第二个则是“no”。
第四个条件关键字是“ifndef”。其语法是:

ifndef <variable-name>

一个实例
├── debug
│ ├── bin
│ │ └── main.exe
│ └── obj
│ ├── main.d
│ ├── main.o
│ ├── device.d
│ ├── device.o
│ ├── a.d
│ └── a.o
├── document
│ ├── doc1.txt
│ └── doc2.txt
├── lib
│ ├── lib1
│ │ ├── lib1.lib
│ │ └── lib1.h
│ └── lib2
│ ├── lib2.lib
│ └── lib2.h
├── makefile
└── src
├── main.c
├── device
│ ├── device.c
│ └── device.h
├── a.c
└── a.h

makefile文件:

DIR_SRC = ./src/ ./src/device/
DIR_LIB = ./lib/lib1/ ./lib/lib1/
DIR_INC := ${DIR_LIB} ${DIR_SRC}
DIR_OBJ = ./debug/obj/
DIR_BIN = ./debug/bin/

empty =
space =${empty} ${empty}
VPATH := ${subst ${space},:,${DIR_SRC}}:${DIR_BIN}
vpath %.c ${subst ${space},:,${DIR_SRC}}
vpath %.o ${DIR_OBJ}
vpath %.d ${DIR_OBJ}

SRC_WITH_PATH := ${foreach n,${DIR_SRC},${wildcard ${n}*.c}}
SRC := ${notdir ${SRC_WITH_PATH}}

OBJ_WITH_PATH := ${patsubst %.c,${DIR_OBJ}%.o,${SRC}}
OBJ := ${notdir ${OBJ_WITH_PATH}}

TARGET = main.exe
TARGET_WITH_PATH := ${DIR_BIN}${TARGET}

CC = gcc
CFLAGS := -g -std=c99 -Wall ${foreach n,${DIR_INC},-I${n}}
LFLAGS := ${foreach n,${DIR_LIB},-L${n}} -llib1

DIR_OBJ ?= ./debug/
TARGET_WITH_PATH ?= ./main.exe

.PHONY:all
all:${TARGET}

${TARGET}:${OBJ:.o=.d} ${OBJ}
	${CC} ${OBJ_WITH_PATH} -o ${DIR_BIN}$@ ${LFLAGS}

%.o:%.c
	${CC} $(CFLAGS) -c -o ${DIR_OBJ}$@ $<

%.d:%.c
	${CC} $(CFLAGS) -MM -MT "$(subst .c,.o,${notdir $<}) $(subst .c,.d,${notdir $<})" -MF "$(subst .c,.d,${DIR_OBJ}${notdir $<})" $<

ifneq ($(MAKECMDGOALS),clean)
-include ${OBJ_WITH_PATH:.o=.d}
endif

.PHONY:clean
clean:
	-rm -f ${DIR_OBJ}*.o
	-rm -f ${DIR_OBJ}*.d
	-rm -f ${TARGET_WITH_PATH}
.PHONY:run
run:${TARGET}
	shell ${TARGET_WITH_PATH}

添加链接描述
https://blog.csdn.net/u013216061/article/details/70592461
添加链接描述
https://blog.csdn.net/zhangyanzlk/article/details/79123562
添加链接描述
https://blog.csdn.net/seven_amber/article/details/70216216
添加链接描述
https://www.w3cschool.cn/mexvtg/dsiguozt.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对“info make”的翻译整理,不是一个纯粹的语言翻译版本,其中对GNU make的一些语法和用法进行了一些详细分析和说明,也加入了一些个人的观点和实践总结。 本书的所有的例子都可以在支持V3.8版本的GNU make的系统中正确执行。 中文于册 伪目标 强制目标(没有命令或依赖的规则) 空目标文件 的特殊目标 多目标 多规则目标 静态模式 静态模式规则的语法 静态模式和隐含规则 双冒号规则 自动产生依赖 第五章:规则的命令 为规则书写命令 命令回显 命令的执行 并发执行命令 命令执行的错误 中断的执行 的递归执行 变量 变量和递归 命令行选项和递归 选项 定义命令包 第六章 中的变量 使用变量 变量的引用 两种变量定义(赋值) 归展开式变量 直接展开式变量 定义一个空格 ”操作符 变量的高级用法 变量的替换引用 变量的套嵌引用 变量取值 如何设置变量 追加变量值 指示符 多行定义 系统环境变量 目标指定变量 模式指定变量 第七章 的条件执行 的条件判断 个例子 条件判断的基本语法 标记测试的条件语句 笫八章:的内嵌函数 的函数 年月日 中文于册 函数的调用语法 文本夂理函数 文件名处理函数 函数 函数 西数 函数 函数 函数 西数 的控制函数 第九章:执行 执行 指定 文件 指定终极日标 替代命令的执行 防止特定文件重建 替换变量定义 使用 进行编译测试 的命令行选项 第十章: 的隐含规则 使用隐含规则 隐含规则的使用 的隐含规则一览 隐含变量 代表命令的变量 命令参数的变量 隐含规则链 模式规 模式规则介绍 模式规则示例 自动化变量 年月日 中文于册 模式的匹配 万用规则 重建内嵌隐含规则 缺省规则 后缀规则 隐含规则搜索算法 笫十一章:使用更新静态库文件 更新静态库文件 库成员作为目标 静态库的更新 更新静态庠的符号索引表 静态库的注意享项 静态库的后缀规则 第十二章: 的特点 的一些特点 源自 的特点 源自其他版本的特点 自身的特点 第十三章和其它版本的兼容 不兼容性 第十四章 的约定 书写约定 基本的约定 规则命令行的约定 代表命令变量 安装目录变量 的标准目标名 安装命令分类 第十五章的常见错误信息 产生的错误信息 附录:关键字索引 可识别的指示符 函数 的自动化变量 环境变量 后序 年月日 中文于册 关于本书 本文瑾献给所有热爱 的程序员!本中文文档版权所有 本文比较完整的讲述 工具,涵盖 的用法、语法。同时重 讨论如何为一个工程编写 作为一个程序员, 工具的使用以及编 写 是必嚅的。系统、详细讲述的中文资料比较少,出于对广大中文 的支持,本人在工作之余,花了个多月时间完成对“ 的翻译整理,完成 这个中文版手册。夲书不是一个纯粹的语言翻译版本,其中对 的一些语法 和用法根据我个人的工作经验进行了一些详细分析和说明,也加入了一些个人的观点和 实践总结。本书的所有的例子都可以在支持版本的 的系统中正确执行。 由于个人水平限制,本文在一些地方存在描述不准确之处。恳请大家在阅读过程中 提出您宝贵的意见,也是对我个人的帮助。我的个人电子邯箱地址: 非常愿意和大家交流!共同学习 阅读本书之前,读者应该对 的工具链和 的一些常用编程工具有一定的 了解。诸如: 等;同时在书写 时,需要能够进行一些 基本的编程。这些工具是维护一个工程的基础。如果大家对这些工具的用法不是 很熟悉,可参考项目资料 阅读本文的几点建议: 如果之前你对 没有了解、当前也不想深入的学习 的读 者。可只阅读本文各章节前半部分的内容(作为各章节的基础知识) 如果你已经对 比较熟悉,你更霄要关心此版本的新增特点、功能、 和之前版本不兼容之处;也可以作为开发过程过程的参考手册。 之前你对 没有概念、或者刚开始接触,本身又想成为一个 下 的专业程序员,那么建议:完整学习本文的各个章节,包括了基础知识和高级 用法、技巧。它会为你在 下的工程开发、工程管理提供非常有用的帮助。 此中文文档当前版本 本文的所有勘误和最新版本可在主 页 上获取!! 谢谢! 徐海兵 年月日 中文于册 第一章:概述 概既述 环境下的程序员如果不会侠用 来构建和管理自己的工程,应该 不能算是一个合柊的专业程序员,至少不能称得上是程序员。在 )环 境下侠用 的 工具能够比较容易的构建一个属于你自己的工程,整个工程的 编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入 些时间去完成一个或者多个称之为 文件的编写。此文件正是 正常工作 的基础 所要完成的 文件描述了整个工程的编译、连接等规则。其中包括:工程 中的哪些源文件需要编译以及如何编译、需要创建那些库文件以及如何创建这些库文 件、如何最后产生我们想要得可执行文件。尽管看起来可能是很复杂的事情,但是为工 程编写
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值