Makefile精简教程

跟我一起写Makefile

GNU make manual

Makefile介绍

一个大型项目的构建,以Makefile描述,从而自动化编译。
Makefile所需描述的就是编译规则和依赖关系。

代码编译和链接

高级语言所编写的代码(如C/C++)必须经过其编译器生成汇编代码,汇编代码经过汇编编译器生成机器代码(中间代码文件,Object File),然後经过链接器生成可执行文件。
编译产生的中间代码文件有很多,为方便使用,可以进行打包以生成库文件,库文件分为静态库和动态库,区别是链接方式。

基本规则

1
2
3
<target> ... : <prerequisites> ...
<command>
...
  • <target>[1] 目标,可以是执行文件或标签
  • <prerequisites> 依赖,所依赖的文件或目标
  • <command> 命令,此规则要执行的命令

如果prerequisites中有文件比target文件要新,则command将要被执行。

Makefile的内容

  • 显式规则
    是说明生成目标,由编写者明确指出
  • 隐晦规则
    是make自动推导,以便简略书写
  • 变量定义
    类似shell环境变量,一般都是字符串
  • 文件指示
    其一,引用其他Makefile文件,其二,根据情况指定有效部分,其三,定义多行命令
  • 注释
    只有行注释,使用'#'字符以注释一行其后内容

make的工作

执行当前目录下的"Makefile"或"makefile"的文件,此文件中的第一个目标作为最终目标,分析依赖关系,执行对应的命令以构建最终目标。

可以显式地指定要生成的最终目标:make target,例如:make clean,此命令通常用以清楚所有中间文件和目标文件。

伪目标和多目标

伪目标不会生成target文件,一般没有依赖文件,可以显式地声明伪目标,只要在伪目标规则之前插入一行:
.PHONY : <target>
注意:伪目标也可以作为第一个目标(默认目标)。

类似的特殊目标还有:

  • .SUFFIXES: <suffixes>
    定义用于后缀规则的后缀列表
  • .INTERMEDIATE : <target>
    依赖作为中间文件(显式指定)
  • .SECONDARY : <target>
    依赖作为次要目标,不会被make自动删除
  • .PRECIOUS : <target>
    依赖作为先决条件,make被终止也不会删除<target>文件,可用于保存中间文件

其他详见GNU make文档。当然,目标也可以有多个,可以配合自动化变量和模式,方便书写。

自动生成依赖关系

可以使用C/C++编译器的功能以自动输出源文件所需的依赖关系("-M"参数),可以将之保存到文件中(可以补充为完整的规则),然後用include关键字引入。

:使用GNU的C/C++编译器,应使用"-MM"参数。

文件包含

make支持三个通配符:'*','?''~',这和shell一样。

文件搜寻

特殊变量VPATH用于指定make的搜寻目录,make先在当前目录寻找,然後到指定目录寻找文件。例如:
VPATH = src:../headers
指定了两个目录"src""../headers",make按此顺序搜索,目录之间以冒号分隔。

还可以使用vpath关键字指定搜寻目录:

  • vpath <pattern> <directories>
    为符合模式<pattern>的文件指定搜索目录<directories>
  • vpath <pattern>
    清除符合模式<pattern>的文件的搜索目录
  • vpath
    清除所有已被设置好了的文件搜索目录
    vpath关键字更加灵活,可以连续使用。

include关键字

使用include关键字可以引用其他Makefile文件,其后跟文件名,可使用路径通配符及变量。
make首先在当前目录下寻找文件,未找到,则寻找参数(--include-dir)指定的目录下,然後寻找<prefix>/include目录下。

MAKEFILES环境变量

此环境变量的值是其他Makefile文件名(以空格分隔),其会在执行make前引入MAKEFILES中的Makefile文件(其中的target无效,且忽略错误)。
不建议使用此环境变量,其会影响每一次make命令。

静态模式

静态模式可以更方便定义多目标规则,语法如下:

1
2
3
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
  • targets 多个目标,可以有通配符
  • target-pattern 目标集模式
  • prereq-patterns 目标的依赖模式

模式target-pattern和prereq-patterns中都应有'%'字符,需要真实的'%'字符则应使用'\'字符转义。

模式使用: 模式字符'%'表示匹配任意长度的非空字符串。

条件判断

条件判断让make根据情况选择不同分支,完整语法如下:

1
2
3
4
5
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

<conditional-directive>是条件表达式,以条件关键字开头,後接两个字符串作参数,参数可以用单/双引号配合空格分隔,或者用小括号配合逗号分隔。其中条件关键字有:

  • ifeq 是否相同
  • ifneq 是否不同
  • ifdef 是否已定义(空赋值会取消定义)
  • ifndef 是否未定义

条件语句不能分成两部分放在两份文件中。

注意:<text-if-true><text-if-false>是可选分支,其可以空格开头,但不应以Tab开头(否则会被认为是命令)。

命令

命令必须以Tab键开头,和shell命令一致,默认以shell解释执行。
通常,make会把要执行的命令输出到终端,以'@'字符在命令前则此命令不会被显示到终端。
当规则目标需要被更新时,make会逐条执行其命令,如果要让下一条命令在上一条基础执行则应同一行上以分号间隔。

命令返回码

每当命令执行完,make会检查命令返回码,命令返回成功则执行下一条命令,若命令出错(命令返回码非零)则终止执行当前规则。

:为了忽略命令出错,可以在命令前面加一个'-'字符。

嵌套make

make执行可以嵌套,以方便维护。
可以定义以下规则:

1
2
target:
$(MAKE) -C subdir

此规则的命令是进入子目录并执行make(附带参数传递)。
$(MAKE)是特殊的宏变量,代表make及其传递参数。

总控Makefile文件的变量可以传递到下级Makefile中,但不会覆盖下级定义的变量(除非指定了"-e"参数)。

要传递变量到下级Makefile中,可以声明:
export <variable ...>;
当然也可以阻止变量的传递:
unexport <variable ...>;

而且使用export关键字同时,可以进行赋值。
如果要传递所有变量,只要一个export就行,有两个变量(SHELL,MAKEFLAGS)必须传递到下级Makefile中。
但是,"-C","-f","-h","-o","-W"参数不会往下传递。

可以在$(MAKE)命令後跟"MAKEFLAGS=..."来设置向下级Makefile文件传递的参数,或者紧接其他需要传递的参数。
"-w"参数会在make输出一些信息,即进入或离开子目录时打印目录信息,使用"-C"参数时,"-w"参数会自动打开,而若有"-s""--no-print-directory"参数则"-w"参数总是无效的。

定义命令包

可以定义命令包,调用时就像变量一样,语法如下:

1
2
3
define <command-package>
...
endef

<command-package>是命令包的名字,可以在目标生成规则中像变量一样使用命令包。例如:

1
2
target : prerequisites
$(<command-package>)

变量

Makefile中定义的变量,和shell变量一样,变量名区分大小写。
变量引用方式:$(var)${var},推荐使用前者(也可以不使用括号)。

变量定义

变量可以用以构建变量的值,变量构建语法如下:
<var> = <expr>
或者:
<var> := <expr>
未定义的变量可以直接使用(但是空值),使用'='赋值的变量的值可以延迟定义(其值引用未定义的变量可以在之後定义,其值也在之後确定),使用":="赋值的变量不能如此(其值引用的变量应在之前已经定义)。

<expr>中的通配符不会展开,如果需要展开,使用wildcard函数即可。

还有一个操作符"?="用于构建变量值,若变量未定义在进行赋值,否则什么也不做。

如果需要定义带空格的变量值,需要使用'#'字符,如下:
<var> = $(nullstring) # comment
$(nullstring)是空变量,结尾的'#'之前的空格也会在var的值中(这需要特别注意)。

多行变量: 使用define关键字,定义方式和命令包一样。

变量操作

变量替换,格式如下:
$(var:a=b)${var:a=b}
即将var中的a替换为b.

变量引用可以嵌套,变量还可以直接拼接,可以用函数处理变量值。

追加变量的值,使用"+="操作符:
<var> += <expr>
追加变量的值,以空格分隔。

override指示符:
make命令行设置的参数是不能直接更改的,使用override指示符以设置这些参数的值,只需在变量赋值表达式(包括多行变量定义)之前加上override修饰符。

环境变量

系统环境变量在make运行时载入,make命令行或者Makefile文件中可以覆盖系统环境变量,若make指定了"-e"参数,则系统环境变量将覆盖Makefile中定义的变量。

当嵌套执行make时,上层Makefile中export的变量会以系统环境变量的方式传递到下级Makefile中。

目标变量

通常定义的是全局变量(作用于整个文件),可以为某个目标设置局部变量,其语法是:
<target ...> : <variable-assignment>
或者:
<target ...> : overide <variable-assignment>

注:<variable-assignment>可以是各种赋值表达式。

模式变量

模式变量是把变量定义在符合其模式的所有目标上,其语法和目标变量一样。
注:make的模式一般至少含有一个'%'的。

自动化变量

模式规则中,目标和依赖都是一系列的文件,每次对模式规则的解析都是不同的目标和依赖文件,使用自动化变量以引用之。

  • $@ 模式规则中的目标文件(一般是一个,也可以有多个)
  • $% 目标中库文件的成员名(目标不是库文件则其值空)
  • $< 模式规则中第一个依赖文件
  • $? 所有比目标新的依赖文件的集合,以空格分隔
  • $^ 所有依赖文件(去除重复),以空格分隔
  • $+ 所有依赖文件(不去除重复)
  • $* 目标模式中'%'字符匹配的及之前的部分
    如果目标没有模式定义,"$*"表示除后缀的部分(make所识别的),或者为空值(make不识别的),注意,此特性是GNU make的,应尽量不使用。

上述七个自动化变量可以加上'D''F',分别表示取目录(dir函数)和取文件名(notdir函数)功能,如:"$(@D)"等。

:自动化变量也可以使用在一般规则中。

函数

函数使用和变量基本一样,函数的值是其返回值,语法如下:
$(<function> <arguments>)
或者:
${<function> <arguments>}

<function>是函数名,<arguments>是函数参数,参数以逗号分隔,推荐使用小括号形式的函数调用语法。

字符串函数

  • $(subst <from>,<to>,<text>)
    字符串替换,<text>中的<from>替换为<to>
  • $(patsubst <pattern>,<replacement>,<text>)
    模式字符串替换,查找<text>中的单词(以空格、Tab、回车或换行分隔)符合模式<pattern>的以<replacement>(也可以是模式)替换
  • $(strip <string>)
    去除空字符,单词间隔的多个空格合并为一个空格且去除首尾的空字符(空格、Tab及不可显示字符)
  • $(findstring <find>,<in>)
    查找字符串,在字符串<in>中查找<find>字符串,成功则返回<find>,否则返回空字符串
  • $(filter <pattern...>,<text>)
    过滤字符串,以<pattern...>模式(可以有多个模式)过滤<text>中的单词,返回符合的字符串
  • $(filter-out <pattern...>,<text>)
    反过滤字符串,返回不符合模式的字符串
  • $(sort <list>)
    字符串排序,返回升序排序後的字符串(会去除相同的单词)
  • $(word <n>,<text>)
    取字符串,取字符串<text>中第<n>个单词,若单词数不足<n>则返回空字符串
  • $(wordlist <ss>,<ee>,<text>)
    取单词串,返回<text>中第<ss>到第<ee>的单词串,若单词数不足则补空字符串
  • $(words <text>)
    计算单词个数
  • $(firstword <text>)
    取首单词

文件名函数

  • $(dir <names...>)
    取目录路径,返回路径(可以多个,依次处理)最后一个'/'及之前的部分,若无'/'字符则返回"./"
  • $(notdir <names...>)
    取文件名,返回路径的非目录部分,与dir函数相反
  • $(suffix <names...>)
    取后缀,返回路径(文件名)序列的后缀(包含'.'字符),若文件无后缀则返回空字符串
  • $(basename <names...>)
    取非后缀部分,返回文件名序列除后缀的部分,与suffix函数相反
  • $(addsuffix <suffix>,<names...>)
    加后缀,把后缀<suffix>加到<names>中的每个单词后面并返回
  • $(addprefix <prefix>,<names...>)
    加前缀,把前缀<prefix>加到<names>中的每个单词前面并返回
  • $(join <list1>,<list2>)
    连接函数,单词序列<list1><list2>按位置两两连接(list1的单词在list2前面)

其他函数

  • foreach函数
    foreach来源于shell的for语句,格式如下:
    $(foreach <var>,<list>,<text>)
    把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再计算<text>所包含的表达式,并将返回值连接(以空格隔开的)成一个字符串返回
  • if函数
    格式:$(if <condition>,<then-part>)
    $(if <condition>,<then-part>,<else-part>)
    <condition>的值是字符串,若此值非空则计算<then-part>返回,否则计算<else-part>返回
  • call函数
    计算参数化表达式,格式如下:
    $(call <expression>,<parm>...)
    <expression>是复杂表达式,其中可以引用参数<parm...>,引用方式为"$(n)"(代表第n个参数)。
    返回的是<expression>的计算结果。注意:<parm...>参数应该去除所有多余的空格(空格也会代入参数)
  • origin函数
    格式:$(origin <variable>)
    <variable>是变量的名字(不应使用引用'$'字符),此函数返回变量的类型(来源,不带引号):
    • "undefined" 未定义
    • "default" 默认定义,如"CC"变量等
    • "environment" 环境变量
    • "file" 此Makefile文件中的变量
    • "command line" 命令行变量
    • "override" override指示符重定义
    • "automatic" 自动化变量
  • shell函数
    shell函数很简单,其后跟shell命令及其参数即可,其和反引号'`'有着相同的功能,即把shell命令输出返回,需注意shell函数会影响性能
  • 控制make的函数
    • $(error <text ...>)
      执行此函数会引发错误以终止make的执行并显示错误信息,此函数可以赋值给变量,由于变量延迟机制,只有需要计算时才会引发错误
    • $(warning <text ...>)
      执行此函数引发警告信息,和error类似,但不会终止make执行

隐含规则

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

隐含规则一览

  1. 编译C程序的隐含规则
    后缀".o"目标依赖".c"文件,生成命令为:
    $(CC) –c $(CPPFLAGS) $(CFLAGS)
  2. 编译C++程序的隐含规则
    后缀".o"目标依赖".cc"".C"文件,生成命令为:
    $(CXX) –c $(CPPFLAGS) $(CFLAGS)
  3. 编译Pascal程序的隐含规则
    后缀".o"目标依赖".p"文件,生成命令为:
    $(CXX) –c $(CPPFLAGS) $(CFLAGS)
  4. 编译Fortran/Ratfor程序的隐含规则
    后缀".o"目标依赖".r",".f"".F"文件,生成命令为:
    ".r": $(FC) –c $(FFLAGS) $(RFLAGS)
    ".f": $(FC) –c $(FFLAGS)
    ".F": $(FC) –c $(FFLAGS) $(CPPFLAGS)
  5. 预处理Fortran/Ratfor程序的隐含规则
    后缀".f"目标依赖".r"".F"文件,生成命令为:
    ".r": $(FC) –F $(FFLAGS) $(RFLAGS)
    ".F": $(FC) –F $(CPPFLAGS) $(FFLAGS)
  6. 编译Modula-2程序的隐含规则
    后缀".sym"目标依赖".def"文件,生成命令为:
    $(M2C) $(M2FLAGS) $(DEFFLAGS)
    后缀".o"目标依赖".mod"文件,生成命令为:
    $(M2C) $(M2FLAGS) $(MODFLAGS)
  7. 汇编编译和预处理的隐含规则
    后缀".o"目标依赖".s"文件,生成命令为:
    $(AS) $(ASFLAGS)
    后缀".s"目标依赖".S"文件,默认使用C预编译器
  8. 链接Object文件的隐含规则
    <n>目标依赖于"<n>.o",生成命令为:
    $(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)
    此规则对于多个Object文件也有效
  9. Yacc C程序时的隐含规则
    后缀".c"目标依赖".y"文件(Yacc文件),生成命令为:
    $(YACC) $(YFALGS)
  10. Lex C程序时的隐含规则
    后缀".c"目标依赖".l"文件(Lex文件),生成命令为:
    $(LEX) $(LFALGS)
  11. Lex Ratfor程序时的隐含规则
    后缀".r"目标依赖".l"文件(Lex文件),生成命令为:
    $(LEX) $(LFALGS)
  12. 从C程序、Yacc文件或Lex文件创建Lint库的隐含规则
    后缀".ln"(lint文件)目标依赖".c"文件,生成命令为:
    $(LINT) $(LINTFALGS) $(CPPFLAGS) -i
    对于依赖为".y"".l"后缀的文件也是类似规则

这些隐含规则(make内建规则),可以不明确写出规则,make自动寻找所需规则和命令。当然也可以只写出依赖关系而不写对应的命令(注意,模式规则和后缀规则要写命令,否则会取消此规则)。

make会推导生成目标的一切方法,而不管中间目标有多少。隐含规则涉及中间目标,当中间目标不存在时才会触发中间规则,产生最终目标後,所产生的中间目标文件都会被删除。

隐含规则的变量

隐含规则的生成命令使用预先设置的变量。

关于命令

  • AR: 函数库打包,默认"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或Ratfor),默认"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: 汇编编译器参数
  • CFLAGS: C语言编译器参数
  • CXXFLAGS: C++语言编译器参数
  • COFLAGS: RCS命令参数
  • CPPFLAGS: C预处理器参数(C和Fortran编译器也会用到)
  • FFLAGS: Fortran语言编译器参数
  • GFLAGS: SCCS的get程序参数
  • LDFLAGS: 链接器参数(如:"ld")
  • LFLAGS: Lex文法分析器参数
  • PFLAGS: Pascal语言编译器参数
  • RFLAGS: Ratfor 程序的Fortran 编译器参数
  • YFLAGS: Yacc文法分析器参数

模式规则

模式可用在目标规则上,便成了模式规则,格式如下:

1
2
3
<target-pattern> : <prereq-patterns>
<command>
...

目标中的'%'表示对文件名匹配(任意非空字符串),其决定了依赖中的'%'的值。

模式规则配合自动化变量使用很方便。

模式规则中'%'匹配的内容称为"茎",依赖的"茎"会传递给目标,当模式匹配包含'/'的文件时,目录部分不参与匹配,文件名部分匹配成功后再把目录部分加回去,于是传递的"茎"也包含目录部分。

可以用模式规则覆盖隐含规则,或者取消内建规则(用空命令覆盖即可)。

后缀规则

后缀规则是老式的隐含规则定义方法,将被模式规则所取代。
后缀规则的后缀应该是make所识别的,有两种形式:

  • 单后缀规则:如".c"相当于"% : %.c"
  • 双后缀规则:如".c.o"相当于"%.o : %.c"

格式如下:

1
2
3
<suffix> :
<command>
...

后缀规则不允许有依赖文件,<suffix>是后缀(包含句点),后缀规则应包含命令,否则将毫无意义。

可以定义自己的后缀列表,以让make识别之,详见.SUFFIXES伪目标

补充

本文档的内容十分丰富,若需简单使用Makefile,只需阅读基本规则和少量其他内容。

make的运行

make执行可以指定目标,参考"make clean"形式。
环境变量"MAKECMDGOALS",存放指定的终极目标列表(命令行指定),若没有指定,则此值是空值。

make的退出码

  • 0 表示成功执行
  • 1 make运行出现错误
  • 2 若使用make的"-q"参数并且make使得一些目标不需要更新,在返回2

GNU Makefile功能

以下可以选择实现:

  • all 此伪目标编译所有目标
  • clean 删除所有make创建的文件
  • install 安装已编译的程序(执行文件拷贝到指定目录)
  • print 列出改变过的源文件
  • tar 源程序打包备份(一般是tar文件)
  • dist 创建压缩文件(一般是tar压缩为gz文件)
  • TAGS 更新所有目标以完整地重编译
  • check和test 用以测试makefile的流程

make的命令行参数

  • "-b","-m"
    忽略和其它版本make的兼容性
  • "-B","--always-make"
    认为所有的目标都需要更新(重编译)
  • "-C <dir>","--directory=<dir>"
    指定Makefile文件目录
  • "-debug[=<options>]"
    输出make的调试信息
  • "-d"
    相当于"–debug=a",输出所有调试信息
  • "-e","--environment-overrides"
    指明环境变量的值覆盖Makefile文件中定义变量的值
  • "-f=<file>","--file=<file>","--makefile=<file>"
    指定执行的Makefile文件
  • "-h","--help"
    显示帮助信息
  • "-i","--ignore-errors"
    在执行时忽略所有的错误
  • "-I <dir...>","--include-dir=<dir>"
    指定搜寻Makefile文件的目录
  • "-j [<jobsnum>]","--jobs[=<jobsnum>]"
    指定并行执行的任务的最大个数
  • "-k","--keep-going"
    出错也不停止运行
  • "-l <load>","--load-average=<load>","-max-load=<load>"
    指定make的运行负载
  • "-n","--just-print","--dry-run","--recon"
    仅输出命令序列但不执行
  • "-o <file>","--old-file=<file>","--assume-old=<file>"
    不重新生成的指定的文件
  • "-p","--print-data-base"
    输出Makefile中的所有数据(包括所有的规则和变量)
  • "-q","--question"
    不运行命令也不输出,仅检查目标是否需要更新
  • "-r","--no-builtin-rules"
    禁止make使用任何隐含规则
  • "-R","--no-builtin-variabes"
    禁止make使用任何作用于变量上的隐含规则
  • "-s","--silent","--quiet"
    在命令运行时不输出命令的输出
  • "-S","--no-keep-going","--stop"
    取消"-k"选项的作用(用以覆盖MAKEFLAGS中选项)
  • "-t","--touch"
    相当于touch命令,仅把目标修改日期更新
  • "-v","--version"
    输出make程序的版本和版权等信息
  • "-w","--print-directory"
    输出运行makefile之前和之后的信息
  • "--no-print-directory"
    禁止“-w”选项
  • "-W <file>","--what-if=<file>","--new-file=<file>","--assume-file=<file>"
    假设目标文件需要更新
  • "--warn-undefined-variables"
    有未定义变量则警告

函数库文件

函数库文件是一个或多个Object文件的打包文件,格式:
archive(member)

多个member以空格分隔,例如:
foolib(hack.o kludge.o)

函数库文件名称上还可以使用通配符,可以使用模式,和普通文件名称基本一样。

注意:进行函数库打包时,小心使用make的并行机制("-j"参数),因为这可能损坏此函数库文件。

特殊符号

符号 说明
'\' 转义(其后一个字符),或者换行(位于行末)
'-' 忽略命令返回码
'@' 关闭命令回显
'~' 表示当前用户目录,或者用以指定用户目录
'*','?' 通配符
'%' 模式字符
'$' 变量或函数引用
  1. 尖括号代表可选项

Makefile精简教程
https://blog.siantao.top/手册/Makefile/Makefile精简教程/
作者
玉水仙楊
发布于
2022年5月1日
许可协议