前言

Linux下的开发,一般都是基于开源的编译器,很多时候并没有太好的IDE,此时非常有必要掌握一门通用的编译构建方法。Makefile应运而生,成功了最流行的Linux下的编译构建方法。Makefile主要是面向代码的编译构建,但是其目标依赖执行的基本原理,也可以用到其他有类似的场景。

1. 概念

1.1. Makefile是什么?

Makefile是一个有规则要求的描述文件,然后通过make工具来解析并执行其中描述的动作。Makefile可以调用相应电脑的命令行来执行一些动作,所以除了不同操作系统的命令行有一些差异外,Makefile的基本规则在所有操作系统中都是一样的。

1.2. Makefile的基本内容

Makefile主要包括两部分,一是变量的定义及展开,另外就是目标和依赖及执行。

1.3. Makefile运行的过程

当我们执行make时,make会在当前目录下找Makefile或makefile的文件,找到之后,就开始解析和执行。因为其根据时间戳来判定是否执行,所以可以达到只编译修改过的源文件,而不用重新编译没有修改过的的源文件。

  • 解析,就是如果有#include指令,则将相应文件添加进来,然后完成变量的定义及展开。
  • 执行,找到终极目标及其依赖,依赖默认是文件,make会检测依赖与目标的时间戳,如果依赖的时间戳比目标的时间戳更新,则开始执行相应的命令,否则不进行执行。此原理可以解决多文件编译时重复编译的问题。

2. 变量

2.1. 变量的定义

> variable=value list

变量命名可是包含字符、数字、下划线,不能含有":“、”#“、”=“或空字符,变量名大小写敏感。变量名,类似C/C++中的宏,所以推荐用全大写加下划线区分单词。
值列表,值可以有多个,以空格分隔。

2.2. 变量的赋值

变量赋值有4种形式:

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
    MAIN := main
    OBJ  := $(MAIN).o
    MAIN := wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    
    输出:

    wmain
    main.o

从输出可以看出,简单赋值是有先后顺序的。

  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
    MAIN = main
    OBJ  = $(MAIN).o
    MAIN = wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    
    输出:

    wmain
    wmain.o

从输出可以看出,递归赋值后面的操作会影响前面的关联变量。所以一般情况下,不建议使用递归赋值。

  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。

    MAIN := main
    OBJ  := $(MAIN).o
    MAIN ?= wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    

    输出:

    main
    main.o

    也可以通过make命令参数来指定条件变量。

    make MAIN=wmain

  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

    MAIN := main
    OBJ  := $(MAIN).o
    MAIN += $(OBJ)
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    

    输出:

    main main.o
    main.o

  • 变量赋值的换行

    OBJS := main.o \
    		add.o \
    		sub.o \
    		comb.o
    

2.3. 变量的引用

变量的引用,即使用变量,使用$(variable)或 ${variable},并且变量可以嵌套引用。变量的引用是一个展开的过程,类似宏的展开。

2.3.1. 基本引用及嵌套引用

	# file name:Makefile
	OBJ = main.o add.o
	NEW_OBJ = $(OBJ)
	ALIAS_OBJ = NEW_OBJ
	ALL_OBJ = $($(ALIAS_OBJ))
	
	# 命令行输入make测试
	test:
		@echo $(OBJ)
		@echo $(NEW_OBJ)
		@echo $(ALIAS_OBJ)
		@echo $(ALL_OBJ)
	```

输出:

main.o add.o
main.o add.o
NEW_OBJ
main.o add.o

2.3.2. 复杂引用

Makefile的变量引用的展开,不仅是单纯的替换,还可以调用一些特殊的函数,使得展开可以完成更复杂的功能。

2.3.2.1. 通配符
通配符使用说明
*匹配0个或者是任意个字符
?匹配任意一个字符
[]我们可以指定匹配的字符放在 “[]” 中
	# 当前目录下有a.c、b.c、ab.c3个源文件
	SOURCE1 := *.c
	SOURCE2 := ?.c
	SOURCE3 := [ab].c
	
	test:
		@echo $(SOURCE1)
		@echo $(SOURCE2)
		@echo $(SOURCE3)
输出
>a.c ab.c b.c
a.c b.c
a.c b.c
2.3.2.2. 特殊通配符

%,在变量展开过程中的模式通配符,表示匹配所有字符。

	DIR := A B
	OBJS := $(patsubst %, %.o, $(DIR))

	test:
		@echo $(OBJS)

输出:

A.o
B.o

2.3.2.3. 函数

在变量引用展开的过程中,可以调用特殊函数。

  • patsubst,模式字符串替换函数。$(patsubst %.c, %.o, $(SOURCES)),将 $(SOURCES)中所有.c结构的文件名替换为.o结尾。

  • subst,字符串替换函数,$(subset AA,aa,AAbbcc),将大写AA替换成小写aa。(注意,后面不要空格)

  • strip,去掉空格,$(strip )。

  • findstring,查找字符串函数,找到即返回目标字符串,否则返回空。

  • filter,过滤函数,$(filter pattern…, text),$(filter %.c %.o,1.c 2.o 3.s),返回1.c 2.o。

  • filter-out,反过滤函数,$(filter-out pattern…,text)。

  • sort,去重排序函数,$(sort list)。

  • notdir,去掉目录,保留文件名。

  • dir,取目录函数。

  • suffix,取后缀扩展名函数,$(suffix names…)。

  • basename,去掉后缀函数,$(basename names…)。

  • addsuffix,添加扩展后缀函数,$(addsuffix suffix,names…)。

  • addprefix,添加前缀函数,可以给文件添加目录,$(addprefix prefix,names…)。

  • join,连接函数,$(join list1, list2)。

  • wildcard,通配符函数,在函数中如果使用了通配符,需要用此函数标记,$(wildcard pattern)。

  • if,条件函数,$(if condition,then-part[,else-part])。

  • or,条件或函数,条件中有一个有效则返回,$(or condition1[,condition2[,condition3…]])。

  • and, 条件与函数,条件都有效,则返回最后一个字符串,$(and condition1[,condition2[,condition3…]])。

  • foreach,遍历列表提取指定元素并执行字符串,$(foreach var,list,text)。

  • file,文件操作函数,可以读写文件,$(file op filename[,text])。

  • call,传递参数并调用新的变量展开,$(call variable,param,param,…)。

  • shell,调用shell命令函数,$(shell cmd param)。

    ifeq ($(OS),Windows_NT)
    #获取当前目录
    CUR_DIR := $(subst  /,\,$(abspath .))
    # 提取当前文件夹名
    CUR_DIR_NAME := $(notdir $(CUR_DIR))
    # 获取当前目录下所有文件夹路径,包括当前文件夹
    SOURCE_DIR	:= $(shell dir . /b /c /s /ad)
    #遍历获取当前目录下所有.c文件
    SOURCES		:= $(wildcard $(patsubst %, %/*.c, $(SOURCE_DIR)))
    # 将所有.c文件列表转为.o列表
    OBJS	:= $(SOURCES:.c=.o)
    else
    #获取当前目录
    CUR_DIR := $(shell pwd)
    # 提取当前文件夹名
    CUR_DIR_NAME := $(notdir $(shell pwd))
    # 获取当前目录下所有文件夹路径,包括当前文件夹
    SOURCE_DIR	:= $(shell find $(CUR_DIR) -type d)
    #遍历获取当前目录下所有.c文件
    SOURCES		:= $(wildcard $(patsubst %, %/*.c, $(SOURCE_DIR)))
    # 将所有.c文件列表转为.o列表
    OBJS	:= $(SOURCES:.c=.o)
    endif
    
    test:
    	@echo $(CUR_DIR)
    	@echo $(CUR_DIR_NAME)
    	@echo $(SOURCE_DIR)
    	@echo $(OBJS)
    
    

2.4. 内置变量

  • AR,打包函数,默认命令为"ar"
  • AS,汇编编译,默认命令为"as"
  • CC,C语言编译,默认命令为"cc"
  • CXX,C++语言编译,默认命令为"g++"
  • CPP,C语言预处理命令,默认为"$(CC) -E”
  • RM,删除文件命令,默认为"rm -f"
  • ARFLGS,打包程序AR的参数,默认为"rv"
  • ASFLAGS,汇编语言编译器参数
  • CFLGS,C语言编译参数
  • CXXFLAGS,C++语言编译器参数
  • LDFLAGS,链接器参数。
    Makefile的隐式规则会调用相应的内置变量,用户如果没有重新赋值,则用默认值。

3. 目标、依赖和命令

3.1. 基础

目标可以1个,也可以多个。依赖可以没有,也可以多个。命令可以没有,也可以多个。目标和依赖即可以是字符量,也可以是变量引用,甚至可以直接使用通配符描述。其规则如下:
注意:command前面必须以真实Tab键(不能是多个空格)隔开,标记后面的内容是命令。

target ... : prerequisites1 ...
	command
	...
	...
	```
prerequisites1 ...: prerequisites11 ...
	command
	...
	...
prerequisites11 ...:
	command
	...
	...

一般情况,目标和依赖,都被make程序当作文件处理。make程序会识别出顶层目标,然后一层一层往下链接。

  • 当依赖文件的时间戳新于目标文件,则make程序会执行下面的命令,来通过依赖项构建新的目标。
  • 当依赖文件的时间戳晚于目标文件,则下面的命令不会执行。
  • 当目标文件不存在时,则会调用命令.
  • 当依赖文件不存在时,表明依赖文件对应的目标需要构建执行,然后再执行当前目标对应的命令。
    当依赖文件被识别到需要构建时,依赖必须作为目标存在,否则会提示相应目标没有规则,报错。

3.2. 多目标与多依赖

3.2.1. 自动化变量

在多目标和多依赖规则中,自动化变量可以自动指代相应变量。

自动化变量说明
$@表示规则的目标文件名。如果目标是一个文档文件(Linux 中,一般成 .a 文件为文档文件,也成为静态的库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名 。
$%当目标文件是一个静态库文件时,代表静态库的一个成员名。
$<规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。
$?所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。
$^代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。一个文件可重复的出现在目标的依赖中,变量“ ” 只记录它的第一次引用的情况。就是说变量“ ^”只记录它的第一次引用的情况。就是说变量“ 只记录它的第一次引用的情况。就是说变量^”会去掉重复的依赖文件。
$+类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。
$*在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时,“茎”也包含目录部分)。

3.2.2. 普通多目标与多依赖

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

$(OBJS): FORCE
	@echo OBJS:$@

输出:

OBJS:main.o
OBJS:add.o
main:main
main:main.o
main:main.o add.o

从示例中可以看出,$(OBJS)是遍历执行的,每次提取一个目标$@,并执行命令。$<只提取依赖项中的第1个,$^则提取所有的依赖项。

3.2.3. 静态模式规则

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

<targets …>:可以省略,target-pattern和prereq-patterns需要用到模式通配符%。%表示匹配当前目标集中的目标,并生成相应的依赖。

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

$(OBJS): %.o: %.c
	@echo OBJS:$@ $<

输出:

OBJS:main.o main.c
OBJS:add.o add.c
main:main
main:main.o
main:main.o add.o

3.3. 伪目标

通常情况下,目标会被识别文件,导致会触发一些隐式编译规则。有时为了避免这种情况,需要标识目标为伪目标,即不对应相应的文件,并且不会被识别为顶层目标。
示例,使用make clean命令来删除所有.o文件。

.PHONE:clean
clean:
	rm -rf *.o

3.4. 隐式规则

当依赖项作为的目标不存在时,make会自动根据依赖项执行相应的命令。

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

输出

cc -c -o main.o main.c
cc -c -o add.o add.c
main:main
main:main.o
main:main.o add.o

make自动根据当前依赖项,查找当前目标中是否有同名的源文件,然后根据源文件的后缀调用相应的默认编译器变量来执行编译。

3.5. 强制执行

有时想强势执行目标,可以有下面三种方法。

  • 当目标文件不存在时,会强制执行
test:
	@echo test
  • 当依赖文件不存在时,会强制执行
test: FORCE
	@echo test

#当FORCE不存在时,会强制执行,习惯用这种方式
FORCE: ;
  • 当依赖项是伪目标时,会强制执行
test: FORCE
	gcc *.c

# 利用伪目标来强制执行	
.PHONE:FORCE
FORCE:;

4. 其他

4.1. 搜索路径

make默认是在当前目录下搜索相关文件,但是有时可能存在相同文件名,或是需要特殊指定搜索目录时,需要有一种方法来指定优先搜索目录及文件。

4.1.1. VPATH

VPATH是Makefile内置的环境变量,默认是空。当VPATH指定目录时,make会优先从VPATH代表地目录去搜索相关文件。

VPATH := src

表示优先从src目录搜索。

VPATH := src dll

表示先从src目录搜索,找不到再从dll目录搜索。

4.1.2. vpath

vpath是Makefile语法的关键字,语法如下:

vpath PATTERN DIRECTORIES
vpath PATTERN
vpath

# 表示从src目录中搜索test.c文件
vpath test.c src
# 表示从src或dll目录中搜索test.c文件
vapath test.c src dll
# 表示从当前目录搜索test.c,如果之前有相关设置,直接清除
vpath test.c
# 恢复默认,相当于清除之前所有设置
vapth

4.2. 条件判断

关键字功能
ifeq判断参数是否不相等,相等为 true,不相等为 false。
ifneq判断参数是否不相等,不相等为 true,相等为 false。
ifdef判断是否有值,有值为 true,没有值为 false。
ifndef判断是否有值,没有值为 true,有值为 false。
语法形式:
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

如Windows下和Linux下的删除命令不同,需要区分。

ifeq ($(OS),Windows_NT)
RM			:= del /q /f
else
RM 			:= rm -f
endif

4.3. 头文件依赖

头文件修改了,包括头文件的源文件则需要重新编译。但是有时,源文件中包含的头文件较多,且头文件中又引用其他头文件。这样复杂的情况下,手动建立一个源文件和头文件的依赖,不太容易。
gcc专门提供了一个编译选项,供用户提取源文件引用的头文件。

gcc -M main.c

会列出所有引用的头文件,包括系统头文件。系统头文件不会改变,不需要建立依赖关系。

gcc -MM main.c > include.txt

会列出所有引用的非系统头文件,并保存到include.txt文件中。
然后在Makefile中,通过include include.txt来将文件中的内容引入Makefile中。

4.4. make命令参数

参数选项功能
-b,-m忽略,提供其他版本 make 的兼容性
-B,–always-make强制重建所有的规则目标,不根据规则的依赖描述决定是否重建目标文件。
-C DIR,–directory=DIR在读取 Makefile 之前,进入到目录 DIR,然后执行 make。当存在多个 “-C” 选项的时候,make 的最终工作目录是第一个目录的相对路径。
-dmake 在执行的过程中打印出所有的调试信息,包括 make 认为那些文件需要重建,那些文件需要比较最后的修改时间、比较的结果,重建目标是用的命令,遗憾规则等等。使用 “-d” 选项我们可以看到 make 构造依赖关系链、重建目标过程中的所有的信息。
–debug[=OPTIONS]make 执行时输出调试信息,可以使用 “OPTIONS” 控制调试信息的级别。默认是 “OPTIONS=b” ,“OPTIONS” 的可值为以下这些,首字母有效:all、basic、verbose、implicit、jobs、makefile。
-e,–enveronment -overrides使用环境变量定义覆盖 Makefile 中的同名变量定义。
-f=FILE,–file=FILE,–makefile=FILE指定文件 “FILE” 为 make 执行的 Makefile 文件
-p,–help打印帮助信息。
-i,–ignore-errors执行过程中忽略规则命令执行的错误。
-I DIR,–include-dir=DIR指定包含 Makefile 文件的搜索目录,在Makefile中出现另一个 “include” 文件时,将在 “DIR” 目录下搜索。多个 “-i” 指定目录时,搜索目录按照指定的顺序进行。
-j [JOBS],–jobs[=JOBS]可指定同时执行的命令数目,在没有 “-j” 的情况下,执行的命令数目将是系统允许的最大可能数目,存在多个 “-j” 目标时,最后一个目标指定的 JOBS 数有效。
-k,–keep-going执行命令错误时不终止 make 的执行,make 尽最大可能执行所有的命令,直至出现知名的错误才终止。
-l load,–load-average=[=LOAD],–max-load[=LOAD]告诉 make 在存在其他任务执行的时候,如果系统负荷超过 “LOAD”,不在启动新的任务。如果没有指定 “LOAD” 的参数 “-l” 选项将取消之前 “-l” 指定的限制。
-n,–just-print,–dry-run只打印执行的命令,但是不执行命令。
-o FILE,–old-file=FILE,
–assume-old=FILE指定 "FILE"文件不需要重建,即使是它的依赖已经过期;同时不重建此依赖文件的任何目标。注意:此参数不会通过变量 “MAKEFLAGS” 传递给子目录进程。
-p,–print-date-base命令执行之前,打印出 make 读取的 Makefile 的所有数据,同时打印出 make 的版本信息。如果只需要打印这些数据信息,可以使用 “make -qp” 命令,查看 make 执行之前预设的规则和变量,可使用命令 “make -p -f /dev/null”
-q,-question称为 “询问模式” ;不运行任何的命令,并且无输出。make 只返回一个查询状态。返回状态 0 表示没有目标表示重建,返回状态 1 表示存在需要重建的目标,返回状态 2 表示有错误发生。
-r,–no-builtin-rules取消所有的内嵌函数的规则,不过你可以在 Makefile 中使用模式规则来定义规则。同时选项 “-r” 会取消所有后缀规则的隐含后缀列表,同样我们可以在 Makefile 中使用 “.SUFFIXES”,定义我们的后缀名的规则。“-r” 选项不会取消 make 内嵌的隐含变量。
-R,–no-builtin-variabes取消 make 内嵌的隐含变量,不过我们可以在 Makefile 中明确定义某些变量。注意:“-R” 和 “-r” 选项同时打开,因为没有了隐含变量,所以隐含规则将失去意义。
-s,–silent,–quiet取消命令执行过程中的打印。
-S,–no-keep-going,–stop取消 “-k” 的选项在递归的 make 过程中子 make 通过 “MAKEFLAGS” 变量继承了上层的命令行选项那个。我们可以在子 make 中使用“-S”选项取消上层传递的 “-k” 选项,或者取消系统环境变量 “MAKEFLAGS” 中 "-k"选项。
-t,–touch和 Linux 的 touch 命令实现功能相同,更新所有的目标文件的时间戳到当前系统时间。防止 make 对所有过时目标文件的重建。
-v,version查看make的版本信息。
-w,–print-directory在 make 进入一个子目录读取 Makefile 之前打印工作目录,这个选项可以帮助我们调试 Makefile,跟踪定位错误。使用 “-C” 选项时默认打开这个选项。
–no-print-directory取消 “-w” 选项。可以是 用在递归的 make 调用的过程中 ,取消 “-C” 参数的默认打开 “-w” 的功能。
-W FILE,–what-if=FILE,–new-file=FILE,–assume-file=FILE设定文件 “FILE” 的时间戳为当前的时间,但不更改文件实际的最后修改时间。此选项主要是为了实现对所有依赖于文件 “FILE” 的目标的强制重建。
–warn-undefined-variables在发现 Makefile 中存在没有定义的变量进行引用时给出告警信息。此功能可以帮助我们在调试一个存在多级嵌套变量引用的复杂 Makefile。但是建议在书写的时候尽量避免超过三级以上的变量嵌套引用。

4.4. 嵌套调用Makefile

ALL_PRJ_OBJ :=  Public/ZLib \
				Public \
				Device \
				MPF	   \
				Main

MAIN_BIN 	:= Bin/main

all:$(ALL_PRJ_OBJ)
	@echo Complete!
	cp -r Bin ..
	
$(ALL_PRJ_OBJ): FORCE
	@cd $@ && make

# 强制更新
FORCE:;

.PHONY : clean run
clean:
	find . -name "*.o" -o -name "*.so" -o -name ".a" | xargs rm -rf
	rm -rf $(MAIN_BIN)

5. 示例

5.1. 编译zlib为libzlib.so动态库

# define the Cpp compiler to use
CXX = gcc

# 动态库需要使用-fPIC
# define any compile-time flags
CXXFLAGS	:= -std=c99 -Wall -Wextra -g -fPIC 

# define library paths in addition to /usr/lib
#   if I wanted to include libraries not in /usr/lib I'd specify
#   their path using -Lpath, something like:
LFLAGS =

# define output directory
OUTPUT	:= ../../Bin

# define source directory
SRC		:= .

# define include directory
INCLUDE	:= ../include

# define lib directory
LIB		:= ../../Bin

CUR_DIR_NAME := $(notdir $(shell pwd))
OBJ 	:= ../../Obj/Public/$(CUR_DIR_NAME)
MAIN	:= lib$(CUR_DIR_NAME).so

SOURCEDIRS	:= $(shell find $(SRC) -type d)
LIBDIRS		:= $(shell find $(LIB) -name "*.so")
FIXPATH = $1
RM = rm -f
MD	:= mkdir -p

# define any directories containing header files other than /usr/include
INCLUDES	:= $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))

# define the C source files
SOURCES		:= $(wildcard $(patsubst %, %/*.c, $(SOURCEDIRS)))

# define the C object files 
OBJECTS		:= $(patsubst $(SRC)/%, $(OBJ)/%, $(SOURCES:.c=.o))

# define the C libs
LIBS		:= 

#
# The following part of the makefile is generic; it can be used to 
# build any executable just by changing the definitions above and by
# deleting dependencies appended to the file from 'make depend'
#
OUTPUTMAIN	:= $(call FIXPATH,$(OUTPUT)/$(MAIN))

# 在make解析规则之前展开执行
OBJ_DIR 	:= $(dir $(OBJECTS))

all: $(OUTPUT) $(OBJ_DIR1) $(MAIN)
	@echo Executing 'all' complete!

$(OUTPUT):
	$(MD) $(OUTPUT)

# 多目标规则,自动遍历符合规则的所有目标
$(OBJ_DIR):
	$(MD) $@

# 动态库需要使用-shared
$(MAIN):$(OBJECTS)
	$(CXX) $(CXXFLAGS) $(INCLUDES) -shared -o $(OUTPUTMAIN) $(OBJECTS)  $(LFLAGS)

# 静态模式规则遍历所有的.o目标
$(OBJ)/%.o:$(SRC)/%.c
	$(CXX) -c $(CXXFLAGS) $(INCLUDES) $< -o $@

# 伪目标,只能手动调用
.PHONY:clean
clean:
	find $(OBJ) -name "*.o" | xargs rm -rf
	rm -rf $(OUTPUTMAIN)

5.2. 编译当前目录所有源文件

遍历当前目录包括子目录所有源文件并编译,并将.o文件生成到Obj目录,引用编译.so文件,将执行文件生成到Bin目录。

# define the Cpp compiler to use
CXX = g++

# define any compile-time flags
CXXFLAGS	:= -std=c++17 -Wall -Wextra -g

# define library paths in addition to /usr/lib
#   if I wanted to include libraries not in /usr/lib I'd specify
#   their path using -Lpath, something like:
LFLAGS =

# define output directory
OUTPUT	:= ../Bin

# define source directory
SRC		:= .

# define include directory
INCLUDE	:= ../include

# define lib directory
LIB		:= ../Bin

# 通过shell获取当前目录,然后通过nodir获取当前目录名
CUR_DIR_NAME := $(notdir $(shell pwd))
OBJ 	:= ../Obj/$(CUR_DIR_NAME)

MAIN	:= main
# 通过shell命令find找到当前目录中所有目录包括子目录
SOURCEDIRS	:= $(shell find $(SRC) -type d)
INCLUDEDIRS	:= $(shell find $(INCLUDE) -type d)
# 找到所有的.so动态库
LIBDIRS		:= $(shell find $(LIB) -name "*.so")
FIXPATH = $1
RM = rm -f
MD	:= mkdir -p

# define any directories containing header files other than /usr/include
INCLUDES	:= $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))

# 通过patsubst函数获取所有目录中的所有.cpp文件并赋给SOURCES
SOURCES		:= $(wildcard $(patsubst %, %/*.cpp, $(SOURCEDIRS)))

# 遍历存在的.cpp文件生成对应的.o文件
OBJECTS		:= $(patsubst $(SRC)/%, $(OBJ)/%, $(SOURCES:.cpp=.o))

# define the C libs
LIBS		:= $(LIBDIRS)

#
# The following part of the makefile is generic; it can be used to 
# build any executable just by changing the definitions above and by
# deleting dependencies appended to the file from 'make depend'
#
OUTPUTMAIN	:= $(call FIXPATH,$(OUTPUT)/$(MAIN))

# 在make解析规则之前展开执行
OBJ_DIR	:= $(dir $(OBJECTS))

# 顶级目标
all: $(OUTPUT) $(OBJ_DIR) $(MAIN)
	@echo Executing 'all' complete!

# 创建输出目录
$(OUTPUT):
	$(MD) $(OUTPUT)

# 多目标规则,自动遍历符合规则的所有目标目录
$(OBJ)/%:
	$(MD) $@

# 生成main
$(MAIN): $(OBJECTS) 
	$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(OUTPUTMAIN) $(OBJECTS) $(LFLAGS) $(LIBS)

# 利用静态模式规则,遍历所有的目标文件并调用相应的.cpp文件
$(OBJ)/%.o:$(SRC)/%.cpp
	$(CXX) -c $(CXXFLAGS) $(INCLUDES) $< -o $@

# 伪目标,只能手动调用
.PHONY:clean
clean:
	find $(OBJ) -name "*.o" | xargs rm -rf
	rm -rf $(OUTPUTMAIN)
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐