makefile基础(GNU)

makefile的核心

targets : prerequisites ; commands...   //不分行的情况

targets : prerequisites                        //分行的情况
  commands

targets : target模式 : prereq模式 ; commands  //静态模式  不分行的情况

targets : target模式 : prereq模式                     //静态模式  分行的情况
  commands...

注:若不分行写,prerequisites与commands间要用分号分隔;若分行写,在依赖项(或规则)和命令块之间不能出现空行,command之前必须要有一个tab字符

targets是一个或多个目标,可以是Object File,也可以是可执行文件,还可以是一个Label(伪目标);可使用通配符,多个目标时,目标之间用空格分隔。

prerequisites是要生成target所需要的文件或目标。

commands是make需要执行的命令。(任意shell命令) 

(1) 在执行commands之前,默认会先打印出该命令,然后再输出命令的结果;如果不想打印出命令,可在各个command前加上@

(2) commands可以为多条,可以分行写,但每行都要以tab键开始;另外,如果后一条命令依赖前一条命令,则这两条命令需要写在同一行,并用分号";"进行分隔

(3) 如果要忽略命令的出错,需要在各个command之前加上减号"-"

只要targets不存在或prerequisites中有一个以上的文件比targets文件新,commands所定义的命令就会被执行

示例:

objects=main.o command.o

edit : $(objects)
    cc -o edit $(objects)

main.o : main.c defs.h
    cc -c main.c
command.o : command.c command.h defs.h
    cc -c command.c

clean:
    rm edit $(objects)

静态模式示例:

objects=main.o command.o

edit : $(objects)
    cc -o edit $(objects)

$(objects) : %.o:%.c

    cc -c $< -o $@  // $<与$@为自动化变量,$<表示所有依赖目标集(main.c command.c),$@表示所有目标集(main.o command.o)

clean:
    rm edit $(objects)

静态库(.a文件)示例:

foolib.a(hack.o kludge.o):hack.o kludge.o

    ar cr foolib.a hack.o kludge.o

    ranlib foolib.a  // 更新foolib.a的符号索引表

make工作原理

a. 读入所有的makefile

b. 读入被include的其他makefile

c. 初始化文件中的变量

d. 推导隐晦规则,并分析所有规则

e. 为所有的目标文件创建依赖关系链(make会一层层地找文件的依赖关系,直到编译出指定的目标)

f. 根据依赖关系,决定哪些目标要重新生成

g. 执行生成命令

Makefile在执行时,会有一个最终目标,其他目标都是被这个目标连带出来!!

(1) 文件搜寻

方法一:在makefile中,使用特殊变量VPATH来设置文件的搜寻目录

VPATH = src:../headers  // make会依次在src、../headers目录中进行文件搜寻

方法二:使用vpath关键字来设置搜寻目录,与VPATH变量相比,vpath更为灵活

vpath %.h ../headers:../include  // 依次在../headers、../include目录中搜索所有.h结尾的文件

vpath %.h  // 清除符合模式%.h的文件的搜索目录

vpath  // 清除所有已被设置好了的文件搜寻目录

连续的vpath语句出现相同的模式,make会按照vpath目录的先后顺序进行文件搜寻

如下:依次在../view、../io目录中搜索所有.c结尾的文件

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

vpath %.c ../view

vpath %.c ../io

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

当然,无论是否定义VPATH变量和vpath语句,make都会最高优先在当前目录中进行文件搜寻

(2) 伪目标(类似c语言里面的goto语句的label;由于伪目标没有依赖项,因此伪目标总会失效而执行对应的命令)

a. 清除工程

clean:

    rm edit $(objects)

更为稳妥的做法(.PHONY表示clean为一个伪目标):

.PHONY: clean

clean:

    -rm edit $(objects)

注:rm前面带个"-"表示在执行命令时某些文件出现问题时不要管,继续做后面的事

     .PHONY指定多个伪目标时,各个伪目标之间用空格分隔(如:.PHONY cleanall cleanobj

b. 执行工程子目录makefile

subsystem:

    cd src/view && make

或者

subsystem:

    make -C src/view  // 使用-C参数时,-w会被自动打开

注:总控makefile的变量可以传递到下层的makefile中,但不会覆盖下层makefile所定义的变量(除非指定-e参数)

      系统变量MAKELEVEL会记录我们当前makefile的调用层数

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

用export修饰的变量会传递到下层makefile;使用unexport修饰的则不会;

若要传递所有变量到下层makefile,只要一个export就行,后面什么都不跟。

另外,系统级环境变量SHELLMAKEFLAGS,总为传递到下层makefile

### 示例1 ###

export var = main.o

#等价于

var = main.o

export var

#等价于

export var := main.o

#等价于

var := main.o

export var

### 示例2 ###

export var += main.o

#等价于

var += main.o

export var

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

约定俗成的伪目标含义说明

all //其功能一般是编译所有的目标
clean // 删除所有被make创建的文件
install // 安装已编译好的程序,其实就是把目标执行文件拷贝到指定目录中
print // 列出改变的源文件
tar // 把源程序打包备份,也就是一个tar文件
dist // 创建一个压缩文件,一般是把tar文件压成Z文件或是gz文件
TAGS // 更新所有的目标,以备完整地重编译使用
check和test // 测试makefile的流程

make命令行参数说明

make之后又3个退出码:0(成功)、1(出现错误)、2(使用了-q参数,且使得一些目标不需要更新)

make    // 不带任何参数。make会在当前目录下找名为GNUmakefile、makefile或Makefile的文件作为输入的make文件,将make文件中的第1个目标作为执行目标

make -h  // 获取make的帮助信息

make -f myMake.mk all   // 将当前目录下的myMake.mk作为输入的make文件,将all作为执行目标

make -I /data/include -I /home/me/makefile   // 在/data/include与/home/me/makefile中搜索子makefile文件(见"子文件包含")

make -i // 忽略makefile中所有命令的错误

make -k // 若某规则的命令出错了,则终止该规则的执行,继续执行其他规则

make -B // 认为所有目标都需要更新(rebuild all)

make -C ~/test -C src  // 指定读取makefile的目录;多个-C参数,则后面以前面作为相对路径(~/test、~/test/src)

make -j 3 // 同时运行命令的个数为3;没有这个参数时,make会能运行多少就运行多少

make -r // 禁止make使用任何隐含规则

make -R // 禁止make使用作用于变量上的隐含规则

make -v // 输出make程序的版本

符号说明

(1) 注释符:#  注释一行。如果要在makefile中使用#,需进行转义,写成#

(2) 换行符:   表示换到下一行,与c++中换行符用法一致。如果要在makefile中使用,需进行转义,写成\

                     注意不要在反斜线后面添加空格

(3) 通配符:*  任意长度的字符串;? 一个字符;如果要在makefile中使用*和?,需进行转义,写成*和?

                []  一个字符,与?区别是,[]限定了范围。

                 如:[bBdp]ig,将与big,Big,dig,pig匹配上

                 如:[0-9]file,将与0file,1file,2file,3file,4file,5file,6file,7file,8file,9file匹配上

                 如:[A-Za-z]*,将与大小写字母开头的名字匹配上

                 如:test[!9],将与除test9之外的test?名字匹配上

               以上3个通配符与Unix、Linux上的Shell是相同的;在makefile中还可以在模式中使用%来表示任意长度的字符串

变量(类似c语言里面的宏;不过make不会在一开始就展开,而是在到了使用时再进行展开)

变量的命名可以包含字符、数字和下划线(可以是数字开头),大小写敏感。

使用变量时需要加上$符号(要用真实的$字符,则用$$来表示),另外建议:用小括号()或大括号{}将变量包括起来,一般使用小括号()来包括。

objects = foo.o bar.o  // 后续通过$(objects)来使用变量

$(objects:.o=.c)  // 返回结果:foo.c bar.c

$(objects:%.o=%.c) // 静态模式方式  返回结果:foo.c bar.c

(1) 变量

var = Begin
foo = $(var)
goo = $(bar)
bar = Hello

printFoo:
    echo $(foo) //打印出Begin,使用前面的变量定义后面的变量
printGoo:
    echo $(goo) //打印出Hello,也可以使用后面的变量来定义前面的变量

注:由于变量可以双向使用,要防止变量展开时无限循环(make能检测出这样错误)

为了避免前面变量使用后面变量的情况,可以使用:=操作符

var := Begin
foo := $(var)
goo := $(bar)  //非法
bar := Hello

(2) 变量的变量

var = Hello
foo = var

printFoo:
    echo $(foo) //打印出Hello

(3) 定义一个空格变量

nullstring :=
space := $(nullstring) #end of the line

注:nullstring为一个空变量,其中什么都没有,space为一个空格(使用nullstring起头,#作为结尾)

(4) ?=操作符

foo ?= bar // 含义是如果foo没有定义过,则foo的值为bar,否则这条语句什么也不做

(5) 把变量的值当成变量

x = y
y = z
a := $($(x)) // $($(x) = $(y) = z,固$(a)=z

另外一个例子:

first_second = Hello
a = first
b = second
all = $($a_$b) // $(all) = Hello

再看一个例子:

dir = foo
$(dir)_src := foo.c

(6) 追加变量值

objects = main.o foo.o【或objects := main.o foo.o】

objects += bar.o  // objects为main.o foo.o bar.o

等价于:objects = $(objects) bar.o【或objects := $(objects) bar.o】

注:若之前objects没有定义,+=自动变成=;

(7) 使用override修改make命令行传入的参数变量(make [var1=value1 var2:=value2 ......])

override var1=myvalue1
override var1:=myvalue1
override var2+=myvalue2

(8) 使用define定义多行变量与命令包的区别是:内部的内容没有以Tab开头;另外,变量的值可以包含函数、命令、文字或其他变量

define foo//等价于foo=bar
bar
endif

另外一个例子:

define two_lines
echo foo
echo $(bar)
endif

(9) 环境变量

对于环境变量,若makefile中已定义了这个变量或这个变量由make命令行带入,则系统的环境变量将被覆盖;

如果make指定了-e参数,则环境变量将覆盖makefile中定义的变量

(10) 目标变量

前面提到的变量均为全局变量,可以在整个文件中访问;可以使用:=、?=、+=、override等

而目标变量是为某个目标设置的局部变量,可以和全局变量重名,因为它的作用范围只在这条规则以及连带规则中

prog : myFlags=-g// 定义myFlags目标变量,其作用范围为prog目标及其连带规则中
prog : foo.o bar.o
    cc $(myFlags) foo.o bar.o
foo.o : foo.c
    cc $(myFlags) foo.c
bar.o : bar.c
    cc $(myFlags) bar.c

(11) 模式变量

由于变量可以定义在某个目标上(见:目标变量),模式变量可以把变量定义在符合这种模式的所有目标上

%.o : myFlags=-g//定义myFlags模式变量,其作用范围为所有以.o结尾的目标及其连带规则中

命令包

define runYacc
    yacc $(firstword $^)
    mv y.tab.c $@
endif

注:runYacc为命令包名称,不要和makefile中的变量重名;yacc总会生成y.tab.c的文件

使用命令包:

foo.c : foo.y
    $(runYacc)

注:$^为foo.y   $@为foo.c

函数

形式:$(<function> <arg1,arg2,...>) 或 ${<function> <arg1,arg2,...>}

(1) 字符串处理函数

$(subst ee,EE,feet on the street)    // 字符串替换函数;将feet on the street中的ee替换成EE,返回结果:fEEt on the street

$(patsubst %.c,%.o,main.c command.c)  // 模式字符串替换函数;将main.c command.c中%.c文件替换成%.o文件,返回结果:main.o command.o

$(strip  a b c )  // 去掉两端空格函数;将" a b c "开头和结尾空格去掉,返回"a b c"

$(findstring a,apple)  // 查找字符串函数;在apple中查找a,返回a字符串(没有找到时,返回空字符串)

$(filter %.c %.s,foo.c bar.c baz.s defs.h)  // 过滤函数;在foo.c bar.c baz.s defs.h中取出%.c与%.s文件,返回结果:foo.c bar.c baz.s

$(filter-out main1.o main2.o,main1.o foo.o main2.o)  // 反过滤函数;在main1.o foo.o main2.o中取出不为main1.o和main2.o的文件,返回结果:foo.o

$(sort foo bar foo lose) // 排序函数;对foo bar foo lose去重后升序排列,返回结果:bar foo lose

$(word 2,foo bar baz) // 取单词函数;取出foo bar baz中的第2个单词,返回结果:bar

$(wordlist 2,3,foo bar baz) // 取单词串函数;取出foo bar baz中的第2-3个单词,返回结果:bar baz

$(words foo bar baz) // 单词个数统计函数;统计foo bar baz中单词的个数,返回结果:3

$(firstword foo bar) // 首单词函数;取出foo bar中的第1个单词,返回结果:foo

(2) 文件名操作函数

$(wildcard src/*.cpp)  // 得到src目录底下所有cpp文件列表,文件之间用空格分隔

$(dir src/foo.c hacks) // 取目录函数;取出src/foo.c hacks中所有文件的目录部分,返回结果:src/ ./

$(notdir src/foo.c hacks) // 取文件函数;取出src/foo.c hacks中所有文件的非目录部分,返回结果:foo.c hacks

$(suffix src/foo.c src2/bar.h hacks) // 取后缀函数;取出src/foo.c src2/bar.c hacks中所有文件的后缀,返回结果:.c .h

$(basename src/foo.c src2/bar.h hacks) // 取前缀函数;取出src/foo.c src2/bar.c hacks中所有文件的前缀,返回结果:src/foo src2/bar hacks

$(addsuffix .c,foo bar) // 加后缀函数;为foo bar中所有文件加上.c后缀,返回结果:foo.c bar.c

$(addprefix src/,foo bar) // 加前缀函数;为foo bar中所有文件加上src/前缀,返回结果:src/foo src/bar

$(join aaa bbb,111 222 333) // 连接函数;为aaa bbb对应加上111 222 333,返回结果:aaa111 bbb222 333

(3) foreach函数

$(foreach n,a b c d,$(n).o)  // 循环函数,其中n为临时变量;返回a.o b.o c.o d.o

(4) if函数

条件为true或非空字符串,执行第1个分支;条件为false或为空字符串,执行第2个分支(可以不写第2个分支)

$(if ifeq ($(CC),gcc),@echo CC is gcc) // 条件函数,如果$CC为gcc,则返回CC is gcc

$(if ifeq ($(CC),gcc),@echo CC is gcc,@echo CC isnot gcc) // 条件函数,如果$CC为gcc,则返回CC is gcc,否则返回CC isnot gcc

(5) call函数

call函数用来做字符串拼接,函数call的参数个数不定 $(call <expression>,<parm1>,<parm2>,<parm3>...)

exp = $(2)+$(1)+$(3)

$(call exp,a,b,c) // exp中$(1)、$(2)、$(3)被参数a、b、c依次取代,最后返回结果:b+a+c

(6) origin函数

origin函数用来检测变量是哪里来的

origin(var1)  // 若变量var1未定义,则返回undefined

origin(CC)  // CC变量为默认定义的,则返回default

origin(var1)  // 若var1为环境变量,且当makefile被执行时,-e参数没有被打开,则返回environment

origin(var1)  // 若var1被定义在makefile中,则返回file

origin(var1)  // 若var1被命令行定义,则返回command line

origin(var1)  // 若var1被override重新定义,则返回override

origin(var1)  // 若var1是一个命令运行中的自动化变量,则返回automatic

(7) shell函数

shell函数用于执行shell命令,并将shell命令的结果作为函数的返回;

$(shell echo *.c)  // 输出当前目录下的所有c文件,并作为结果返回 

(8) 控制make的函数

$(error makefile is error!)  // make退出。产生一个error,打印出:makefile is error!

$(warning remember update your code from svn before make.)  // 只产生一个warning,打印出:remember update your code from svn before make.

(9) 自动化变量 -- 为模式规则所使用

$@:表示规则中的目标文件集合

$(@D):$@的目录部分;若$@为"dir/src/foo.o",则$(@D)为"dir/src"

$(@F):$@的文件部分;若$@为"dir/src/foo.o",则$(@F)为"foo.o"

$%:仅当目标是函数库文件(.a)文件时,表示规则中的目标成员名,否则为空;如:foo.a(bar.o),则$%为bar.o;$(%D)、$(%F)与$(@D)、$(@F)类似

$<:所有依赖目标的挨个值;为了避免麻烦,最好写成$(<);$(<D)、$(<F)与$(@D)、$(@F)类似

$?:所有比目标新的依赖目标的集合,以空格分隔;$(?D)、$(?F)与$(@D)、$(@F)类似

$^:所有依赖目标的集合(会去重),以空格分隔;$(^D)、$(^F)与$(@D)、$(@F)类似

$+:所有依赖目标的集合(不会去重),以空格分隔;;$(+D)、$(+F)与$(@D)、$(@F)类似

$*:表示目标模式中%及之前的部分;若目标是"dir/a.foo.b",且目标的模式为"a.%.b",那么$*的值为"dir/a.foo";$(*D)、$(*F)与$(@D)、$(@F)类似

条件判断

(1) ifdef-else-endif  // 如果定义【ifndef-else-endif  // 如果未定义】

ifdef do_sort
func := sort
else
func := strip
endif

bar := a d b g q c
foo := $($(func) $(bar))

注:如果定义了do_sort,则foo := $(sort a d b g q c)  // 返回a b c d g q

      若没有定义,则foo := $(strip a d b g q c)  // 去掉字符串两端的空格

(2) ifeq-else-endif // 如果相等【ifneq-else-endif  // 如果不相等】

libs_for_gcc = -lgnu
normal_libs =

foo:$(objects)
ifeq ($(CC),gcc)//或ifeq '$(CC)' 'gcc'或ifeq "$(CC)" "gcc"或ifeq '$(CC)' "gcc'
    $(CC) -o foo $(objects) $(libs_for_gcc)
else
    $(CC) -o foo $(objects) $(normal_libs)
endif

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

sources = foo.c bar.c
ifneq ($(MAKECMDGOALS),clean)//MAKECMDGOALS为环境变量,存放的是make指定的终极目标列表(没有指定时为空值)
include $(sources:.c=.d)
endif

规则

(1) 模式规则

 %.o:%.c //也可以使用老式风格双后缀写法:.c.o

    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

 注:将所有的c文件都编译成.o文件    $@:表示所有的目标的挨个值  $<:表示所有依赖目标的挨个值

另外一个例子:静态库文件(.a文件)生成

%.a:%.c //也可以使用老式风格双后缀写法:.c.a

    $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o

    $(AR) r $@ $*.o

    $(RM) $*.o

(2) 隐含规则

a. make看到一个.o文件就会自动把其对应.c文件加到依赖关系中(如:foo.o会推导出foo.c),

并且后面的命令cc -c foo.c -o foo.o也会被推导出来

隐含的编译命令会使用一些系统变量,如:CFLAGS(用来控制编译时编译器参数)

foo.o : foo.c defs.h

    cc -c foo.c -o foo.o

//可以直接简写成

foo.o : defs.h

b. 对于源文件的依赖关系可能需要一系列的头文件,比如main.c中有一句#include "defs.h"

    为了让make自动找源文件中包含的头文件来生成依赖关系,我们可以执行如下命令:cc -M main.c

    该命令会输出:main.o : main.c defs.h

    大多数c/c++编译器可直接使用-M;对于gcc编译器,需要使用-MM参数,直接使用-M会将标准库的头文件也包含进来

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

    如何利用编译器的这个功能来自动生成依赖关系,让makefile自己依赖源文件?这个不现实,不过我们可以迂回地实现这一功能。

    GNU组织建议把编译器为每个源文件自动生成的依赖关系放到一个文件中,为每个xxx.c生成一个xxx.d的makefile

%.d:%.c

   @set -e; rm -f $@;

   $(CC) -M $(CPPFLAGS) $< > $@.$$$$;

   sed 's,($*).o[ :]*,1.o $@ :,g' < $@.$$$$ > $@;

   rm -f $@.$$$$

     @set -e:如果一个命令失败,就退出

     $<:%.c文件   $@:模式%.d文件   $$$$为一个随机编号

     sed那句话要做的事就是将编译器生成的依赖关系也加入到.d文件的依赖,如:main.o : main.c defs.h转成main.o main.d : main.c defs.h

     这样.d文件也会自动更新了,并会自动生成了。除了在.d文件中加入依赖关系,还可以将生成命令一并加入。

     最后使用include命令来引入.d的makefile

sources = foo.c bar.c

include $(sources:.c=.d)

c. 常用的隐含规则

   C程序的隐含规则:.o依赖目标自动推导为.c,并且生成命令为:$(CC) -c $(CPPFLAGS) $(CFLAGS)

   C++程序的隐含规则:.o依赖目标自动推导为.c或.cc,并且生成命令为:$(CXX) -c $(CPPFLAGS) $(CFLAGS)  // 建议使用.cc作为C++源文件后缀

   链接Object文件的隐含规则:XX目标依赖于XX.o,其生成命令:$(CC) $(LDFLAGS) XX.o $(LOADLIBES) $(LDLIBS)

d. 隐含规则中推导出来的中间目标,默认在最终目标成功产生时会被删除

    可以通过伪目标".SECONDARY:sec"来阻止make删除名为sec的中间目标

    可以通过伪目标".PRECIOUS:%.o"来保存被隐含规则所生成的中间目标

    一个被makefile指定成目标或依赖目标的文件不能被当做中间目标,但可以使用伪目标".INTERMEDIATE"来强制声明(.INTERMEDIATE:mid)

f. 可以通过显示写出规则,来重载make内建的隐含规则

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

子文件包含(与c++一样,会将子make文件展开安插到当前位置)

如果文件没有指定绝对或相对路径,make还会在以下目录中寻找(make -I参数指定的目录;/usr/gnu/include;/usr/local/include;/usr/include

include myMake.mk  // 在当前位置载入myMake.mk

include foo.make *.mk $(testmk)  // 若testmk=myMake.make,且*.mk能找到a.mk b.mk,那么当前语句会依次载入foo.make a.mak b.mak myMake.make

在寻找子make文件时,若没有找到,make会生成一条警告信息,不会马上出现致命错误;它会继续载入其他文件,一旦完成所有子make文件的读取,make会再重试那些没找到的子make文件,如果还是不行,make才会出现一条致命信息,然后终止执行。

如果想让make不理会那些无法载入的子make文件而继续执行,可以在include前加一个"-",如:

-include myMake.mk

环境变量MAKEFILES:这是个全局变量,所有的make都会include该变量的值来载入MAKEFILES指定的子make文件(多个子make文件时,用空格分隔)

与include不同的是:MAKEFILES引入的makefile的目标不起作用,如果环境变量定义的文件发现错误,make也不会理会。

环境变量MAKEFILE_LIST:在对include的子makefile进行解析执行前,会将子makefile名追加到MAKEFILE_LIST中

makefile调试

make -n  // 对makefile文件只是显示命令,不执行命令 

make -W foo.c // 后面跟的文件一般是源文件或依赖文件,make会根据规则推导来运行依赖于这个文件的命令

make -s  // 执行当前的makefile文件时,不打印出命令本身的内容

make -t // 把目标文件的时间更新,但不更改目标文件(make假装编译,把目标改成已编译过的状态) 

make -q // 找目标。若目标存在,什么都不输出;不存在,则打印出一条出错信息 

make -o main.o // 不重新生成main.o,即使这个目标的依赖文件比它要新

make -p // 执行makefile,输出makefile中的所有数据,包括所有的规则和变量

make -qp // 只输出信息而不执行makefile

make -p -f /dev/null // 输出的信息包含着makefile文件的文件名和行号

make -w  // 对于嵌套多层的makefile,在执行下级makefile之前,会打印:进入当前工作目录;执行完后,会打印:退出当前工作目录

make --warn-undefined-variables  // 只要make发现未定义变量,就输出警告信息

make --debug=a  // 输出make的所有调试信息(会非常多)

make --debug=b  // 输出不需要重编译目标的信息

make --debug=v  // 输出信息:哪个make被解析,不需要被重编译的依赖文件或依赖目标等

make --debug=i  // 输出所有的隐含规则

make --debug=j  // 输出命令的详细信息,如命令的PID、返回码等

make --debug=m  // 输出make读取makefile,更新makefile,执行makefile的信息

.VARIABLES  // 该变量的值为调用点之前makefile中定义所有全局变量列表

参考

http://www.gnu.org/software/make/manual/

http://www.gnu.org/software/autoconf/manual/autoconf.html

跟我一起写Makefile(一)--陈皓

跟我一起写Makefile(二)--陈皓

跟我一起写Makefile(三)--陈皓

跟我一起写Makefile(四)--陈皓

跟我一起写Makefile(五)--陈皓

跟我一起写Makefile(六)--陈皓

跟我一起写Makefile(七)--陈皓

跟我一起写Makefile(八)--陈皓

跟我一起写Makefile(九)--陈皓

跟我一起写Makefile(十)--陈皓

跟我一起写Makefile(十一)--陈皓

跟我一起写Makefile(十二)--陈皓

跟我一起写Makefile(十三)--陈皓

跟我一起写Makefile(十四)--陈皓

原文地址:https://www.cnblogs.com/kekec/p/3538019.html