Makefile基础
文章目录
1 编译原理
1.1 编译过程
四个阶段:预处理(生成.i文件)——>编译(生成.s文件)——>汇编(生成.o文件)——>链接(生成可执行文件)
1.2 编译过程的命令
预处理阶段:
gcc -E main.c # 不会生成 .i 文件
gcc -E main.c -o helloworld.i # 生成 .i 文件
- -E 选项告诉编译器只进行预处理操作
- -o 选项把预处理的结果输出到指定文件
编译阶段:
gcc -S main.i
gcc -S main.i -o main.s
- -S 选项告诉编译器,将预处理文件编译成汇编文件
汇编阶段:
gcc -c main.c
gcc -c main.c -o main.o
- -c 选项告诉编译器,将汇编文件编译为二进制的目标文件
链接阶段:
gcc main.o
gcc main.o -o main
gcc main.c -o main
- 将目标文件或者源文件编译为最终的可执行文件
1.3 编译选项
|> 编译选项
-m64
: 指定编译为 64 位应用程序-std=
: 指定编译标准,例如:-std=c++11、-std=c++14-g
: 包含调试信息-w
: 不显示警告-O
: 优化等级,通常使用:-O3-I
: 加在头文件路径前fPIC
: (Position-Independent Code), 产生的没有绝对地址,全部使用相对地址,代码可以被加载到内存的任意位置,且可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的
|> 链接选项-l
: 加在库名前面-L
: 加在库路径前面-Wl,<选项>
: 将逗号分隔的 <选项> 传递给链接器-rpath=
: “运行” 的时候,去找的目录。运行的时候,要找 .so 文件,会从这个选项里指定的地方去找
1.4 默认规则
- CC: Program for compiling C programs; default cc
用于指定编译 C 语言程序的编译器,默认是cc
。通过设置这个变量来更改使用的 C 编译器。 - CXX: Program for compiling C++ programs; default g++
用于指定编译 C++ 程序的编译器,默认是g++
。通过设置这个变量来更改使用的 C++ 编译器。 - CFLAGS: Extra flags to give to the C compiler
用于指定额外的标志(flags)给 C 编译器。通过CFLAGS
设置编译器的优化选项或者警告选项。 - CXXFLAGS: Extra flags to give to the C++ compiler
用于指定额外的标志给 C++ 编译器,类似于CFLAGS
,但专门用于 C++。 - CPPFLAGS: Extra flags to give to the C preprocessor
用于指定额外的标志给 C 预处理器。这可以包括宏定义、头文件搜索路径等。通常在这里设置-I
标志以添加头文件搜索路径。 - LDFLAGS: Extra flags to give to compilers when they are supposed to invoke the linker
用于指定额外的标志给链接器。这可以包括库搜索路径、链接的库文件等。例如,你可以通过LDFLAGS
添加链接库的路径。
示例:
CFLAGS=-O2 -Wall
LDFLAGS=-L/usr/local/lib
1.4 库
静态库的编译:
# 先编译为.o文件
gcc -c [.c] -o [自定义文件名]
gcc -c [.c] [.c] ...
# 再编译为静态库文件.a
ar cr [lib库名.a] [.o] [.o] ... # cr表示create and replace,若不存在则创建,存在则替换
# 链接成可执行文件
gcc [.c] [.a] -o [自定义输出文件名]
gcc [.c] -o [自定义输出文件名] -l[库名] -L[库所在路径]
动态库的编译:
# 先编译为.o文件
gcc -c -fpic [.c/.cpp][.c/.cpp]...
# 再编译为动态库文件.so
gcc -shared [.o][.o]... -o [lib自定义库名.so]
# 链接库到可执行文件
gcc [.c/.cpp] -o [自定义可执行文件名] -l[库名] -L[库路径] -Wl,-rpath=[库路径] # -Wl, -rpath=[库路径] 可省略
2 Makefile
2.1 基本格式
targets : prerequisties
command (tab键)
- target:目标文件,可以是 OjectFile,也可以是执行文件,还可以是一个标签(Label)
- prerequisite:生成 target 所需要的依赖项。
- command:是 make 需要执行的命令
Makefile
debug :
@echo hello # 使用tab键,若不加@则会先输出命令"echo hello",再输出命令执行的结果"hello"
终端执行:
make debug
hello
2.2 基本规则
- make 会在当前目录下找到一个名字叫
Makefile
或makefile
的文件 - 如果找到,它会找文件中第一个目标文件(target),并把这个文件作为最终的目标文件
- 如果 target 文件不存在,或是 target 文件依赖的 .o 文件(prerequities)的文件修改时间要比 target 这个文件新,就会执行后面所定义的命令 command 来生成 target 这个文件
- 如果 target 依赖的 .o 文件(prerequisties)也存在,make 会在当前文件中找到 target 为 .o 文件的依赖性,如果找到,再根据那个规则生成 .o 文件
- 伪目标:为了避免 target 和 Makefile 同级目录下
文件/文件夹
重名的这种情况,可以使用一个特殊的标记.PHONY
来显式地指明一个目标是 “伪目标”,向 make 说明,不管是否有这个文件/文件夹,这个目标就是 “伪目标”
如,在当前文件夹下存在一个叫clean的文件,然后我的目标文件也是clean
clean :
@rm -rf deleted
.PHONY : clean # 生成一个伪目标
2.3 变量
**变量的初始化与引用:变量在声明时需要给予初值(空值也是初值),而在使用时,需要给在变量名前加上 $
符号,并用小括号 ()
或者{}
把变量给包括起来。
2.3.1 变量的定义与引用
cpp := src/main.cpp # 直接赋初值
obj := objs/main.o
# 使用()或者{}
$(obj) : ${cpp}
@g++ -c $(cpp) -o $(obj)
compile : $(obj)
make complie # 编译:main.cpp ——> main.o
预定义变量:
$@
: 目标(target)的完整名称$<
: 第一个依赖文件(prerequisties)的名称$^
: 所有的依赖文件(prerequisties),以空格分开,不包含重复的依赖文件
cpp := src/main.cpp
obj := objs/main.o
$(obj) : ${cpp}
@g++ -c $< -o $@
@echo $^
compile : $(obj)
.PHONY : compile
2.4 常用符号
2.4.1 = 简单赋值
- 简单的赋值运算符
- 用于将右边的值分配给左边的变量
- 如果在后面的语句中重新定义了该变量,则将使用新的值
HOST_ARCH = aarch64
TARGET_ARCH = $(HOST_ARCH)
# 更改了变量 a
HOST_ARCH = amd64
debug:
@echo $(TARGET_ARCH)
注:与 := 的区别
VAR1 = one
VAR2 = $(VAR1) two
VAR1 = three
# 使用 :=
VAR3 := four
VAR4 := $(VAR3) five
VAR3 := six
all:
@echo "Using =:"
@echo "VAR1 = $(VAR1)"
@echo "VAR2 = $(VAR2)"
@echo ""
@echo "Using :=:"
@echo "VAR3 = $(VAR3)"
@echo "VAR4 = $(VAR4)"
Using =:
VAR1 = three
VAR2 = three two
Using :=:
VAR3 = six
VAR4 = four five # 使用了VAR3一开始展开的值
=
赋值: 这是最基本的赋值方式,也是最简单的。它会在使用变量的地方展开所有引用。:=
赋值: 这是一种更高效的赋值方式。它会在赋值时展开右侧的变量,但在后续使用时不再重新展开。- 在大型项目中,为了避免不必要的性能开销,常常会使用
:=
赋值方式
2.4.2 ?= 默认赋值
- 默认赋值运算符
- 如果该变量已经定义,则不进行任何操作
- 如果该变量尚未定义,则求值并分配
HOST_ARCH = aarch64 # "HOST_ARCH =" 也算赋值
HOST_ARCH ?= amd64
debug:
@echo $(HOST_ARCH)
2.4.3 += 累加赋值
include_path := src
CXXFLAGS := -m64 -fPIC -g -O0 -std=c++11 -w -fopenmp
CXXFLAGS += $(include_paths) # CXXFLAGS = CXXFLAGS + $(include_paths)
debug :
@echo $(CXXFLAGS)
-m64 -fPIC -g -O0 -std=c++11 -w -fopenmp src
2.4.4 \ 续行符
LDLIBS := cudart opencv_core \
gomp nvinfer protobuf cudnn pthread \
cublas nvcaffe_parser nvinfer_plugin
cudart opencv_core gomp nvinfer protobuf cudnn pthread cublas nvcaffe_parser nvinfer_plugin
2.4.5 */% 通配符
`*`: 通配符表示匹配任意字符串,可以用在目录名或文件名中
`%`: 通配符表示匹配任意字符串,并将匹配到的字符串作为变量使用
2.5 常用函数
书写规则:
$(fn, arguments) or ${fn, arguments}
- fn: 函数名
- arguments: 函数参数,参数间以逗号
,
分隔,而函数名和参数之间以“空格”分隔 - 常见函数:shell、subst、patsubst、foreach、dir
- 其他函数:notdir、filter、basename
目录结构如下:
![[Pasted image 20231130230923.png]]
2.5.1 shell
shell命令函数
$(shell <command> <arguments>)
- 名称:shell 命令函数 —— shell
- 功能:调用 shell 命令 command
- 返回:函数返回 shell 命令 command 的执行结果
示例:
# 1. 找到src目录下的.cpp文件: shell
cpp_srcs := $(shell find src -name *.cpp)
debug :
@echo $(cpp_srcs)
.PHONY : debug
make debug
src/func1.cpp src/func2.cpp src/func3.cpp
2.5.2 subst
替换字符串函数
$(subst <from>,<to>,<text>)
- 名称:字符串替换函数——subst
- 功能:把字串 <\text> 中的 <\from> 字符串替换成 <\to>
- 返回:函数返回被替换过后的字符串
示例:
# 2. subst : 字符串替换函数
cpp_srcs := $(shell find src/ -name *.cpp)
cpp_objs := $(subst src/,objs/,$(cpp_srcs)) # 替换目录名为objs
cpp_objs := $(subst .cpp,.o,$(cpp_objs)) # 替换文件名为.o
debug :
@echo $(cpp_srcs)
@echo $(cpp_objs)
make debug
src/func1.cpp src/func2.cpp src/func3.cpp
objs/func1.o objs/func2.o objs/func3.o
2.5.3 patsubst
$(patsubst <pattern>,<replacement>,<text>)
- 名称:模式字符串替换函数 —— patsubst
- 功能:通配符
%
,表示任意长度的字串,从 text 中取出 patttern, 替换成 replacement - 返回:函数返回被替换过后的字符串
示例:
# 3. patsubst : 模式字符串替换函数,一次性可以替换多个文件
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs)) # 相当于subst的组合效果
debug :
@echo $(cpp_srcs)
@echo $(cpp_objs)
make debug
src/func1.cpp src/func2.cpp src/func3.cpp
objs/func1.o objs/func2.o objs/func3.o
2.5.4 foreach和:%=%
循环遍历字符串,空格作为分隔符
$(foreach <var>,<list>,<text>)
- 名称:循环函数——foreach。
- 功能:把字串<list>中的元素逐一取出来,执行<text>包含的表达式
- 返回:<text>所返回的每个字符串所组成的整个字符串(以空格分隔)
示例:
# 4. foreach: 循环函数——> 替换{变量:%=-I%}
include_paths := /usr/include \
/usr/local/include
include_paths_foreach := $(foreach item,$(include_paths),-I$(item)) # 以空格符作为分割符
# 使用{变量:%=-I%}
include_paths_I = $(include_paths:%=-I%) # 和foreach效果一样,推荐这种写法
debug :
@echo $(include_paths_foreach)
@echo $(include_paths_I)
make debug
-I/usr/include -I/usr/local/include
-I/usr/include -I/usr/local/include
2.5.5 dir
返回目录的路径或者从路径中提取目录路径
$(dir <names...>)
- 名称:取目录函数——dir。
- 功能:从文件名序列<\names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前
的部分。如果没有反斜杠,那么返回“./”。 - 返回:返回文件名序列<\names>的目录部分。
示例:
# 5. dir: 取目录函数
# 先删除objs目录
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))
debug :
@echo $(cpp_srcs)
@echo $(cpp_objs)
# 目的:将src目录下的.cpp文件编译生成.o目标文件存放在objs目录下
objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@
compile : $(cpp_objs) # make compile 编译命令,找到依赖项cpp_objs-=objs/%.o,执行该编译命令
make compile
![[Pasted image 20231130232129.png]]
2.5.6 notdir和filter
- notdir: 在一个绝对路径中取最后一个非目录的文件名称
- filter: 按照一定的规则对字符串进行过滤
# notdir
$(notdir <names...>)
# filter
$(filter <names...>)
示例:
# 6. notdir:在/usr/lib目录下,分别取静态文件名和动态文件名
libs := $(notdir $(shell find /usr/lib -name lib*))
# 7 filter:过滤出静态或者动态文件
a_libs := $(filter %.a,$(libs))
so_libs := $(filter %.so,$(libs))
debug :
@echo $(a_libs)
@echo $(so_libs)
make debug
![[Pasted image 20231130232450.png]]
2.5.7 basename
取文件名(不包括后缀)
$(basename <names...>)
示例:
# 7 filter:过滤出静态或者动态文件
a_libs := $(filter %.a,$(libs))
so_libs := $(filter %.so,$(libs))
# debug :
# @echo $(a_libs)
# @echo $(so_libs)
# 8 basename: 取库名(不包含后缀)
a_libs_basename := $(basename $(a_libs))
so_libs_basename := $(basename $(so_libs))
debug :
@echo $(a_libs_basename)
@echo $(so_libs_basename)
make debug
![[Pasted image 20231130232754.png]]
示例:
# 去掉指定的字符串,使用subst
libs_name := $(subst lib, ,$(a_libs_basename))
debug :
@echo $(libs_name)
2.5.8 filter-out
剔除字符串
$(filter-out <names...>)
示例:
# 8 filter-out: 剔除objs/func1.o字符串
cpp_objs := $(shell find objs -name *.o)
cpp_objs_filtered = $(filter-out objs/func1.o,$(cpp_objs))
debug :
@echo $(cpp_objs)
@echo $(cpp_objs_filtered)
make debug
objs/func1.o objs/func2.o objs/func3.o
objs/func2.o objs/func3.o
2.5.9 wildcard
通配符
示例:
cpp_srcs := $(wildcard src/*.cc src/*.cpp src/*.c)
debug :
@echo $(cpp_srcs)
make debug
src/func1.cpp src/func2.cpp src/func3.cpp
示例 1 编译四个阶段的程序
- 编译四个阶段的程序
目录结构:
Test
-src
-main.cpp
# 用于检查每个编译阶段的输出
debug :
@echo $(cpp_exe)
# 清空指定文件夹下的文件
clean :
@rm -rf src/*.i
# 1 进行预编译
cpp_srcs := $(shell find src -name *.cpp)
cpp_I := $(patsubst src/%.cpp,src/%.i,$(cpp_srcs))
src/%.i : src/%.cpp
@g++ -E $^ -o $@
preprocess : $(cpp_I)
# 2 进行汇编
cpp_s := $(patsubst src/%.i,src/%.s,$(cpp_I))
src/%.s : src/%.i
@g++ -S $^ -o $@
assemble : $(cpp_s)
# 3 目标文件生成
cpp_o := $(patsubst src/%.s,objs/%.o,$(cpp_s))
objs/%.o : src/%.s
@mkdir -p $(dir $@)
@g++ -c $^ -o $@
object : $(cpp_o)
# 4 链接可执行文件
cpp_exe := $(patsubst objs/%.o,bin/%,$(cpp_o))
bin/% : objs/%.o
@mkdir -p $(dir $@)
@g++ $^ -o $@
executable : $(cpp_exe)
# 运行
run : bin/main
@./$<
.PHONY : debug clean preprocess assemble object executable run
make run
经过一系列编译链接后生成的目录
![[Pasted image 20231201094511.png]]
示例 2 编译带头文件的程序
- 编译带头文件的程序
目录结构:
Test2
-include
-add.h
-minus.h
-src
-main.cpp
-add.cpp
-minus.cpp
Makefile
// add.h
#ifndef ADD_H
#define ADD_H
int add(int, int);
#endif // !ADD_H
// add.cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
// minus.h
#ifndef MINUS_H
#define MINUS_H
int minus(int, int);
#endif // !MINUS_H
// minus.cpp
#include "minus.h"
int minus(int a, int b)
{
return a - b;
}
// main.cpp
#include<iostream>
#include "add.h"
#include "minus.h"
int main()
{
int a = 11;
int b = 12;
int add_res = add(a, b);
int minus_res = minus(a, b);
std::cout << "a + b = " << add_res << "\n";
std::cout << "a - b = " << minus_res << "\n";
return 0;
}
Makefile编译:
# 1 通过源文件的文件名替换为目标文件的文件名
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))
# 2 编译源文件为目标文件
# 先设置库文件的路径,自定义的头文件若要被包含需要指定头文件的路径,不然编译器会找不到
include_path := /home/gaoxl/vscode/makefile_learning/Test2/include
# 加上-I
I_flags := $(include_path:%=-I%)
# 加上编译选项,先创建目录,不然编译会报错
compile_options := -std=c++11 -g -O3 -w $(I_flags)
objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@ $(compile_options)
# 3 生成可执行文件,先创建目录,不然编译会报错
bin/exec : $(cpp_objs)
@mkdir -p $(dir $@)
@g++ $^ -o $@
# 4 通过run执行
run : bin/exec
@./$<
# 5 补充:再次编译时清空objs下的目标文件
clean : objs/%.o
rm -rf $^
debug :
@echo $(cpp_srcs)
@echo $(cpp_objs)
@echo $(I_flags)
@echo $(compile_options)
.PHONY : debug run clean
make run
生成目录:
![[Pasted image 20231201105421.png]]
结果:
a + b = 23
a - b = -1
示例3 静态库的编译
目录结构:
Test3
-src
-main.cpp
-add.cpp
-minus.cpp
-include
-add.h
-minus.h
Makefile
编译流程:先将add.cpp
minus.cpp
编译为目标文件,存放在objs目录下(编译时主要需要用到源文件的头文件路径);然后将objs目录下的目标文件add.o``minus.o
编译为静态文件;最后,与main.cpp
或者main.o
文件与静态文件链接为可执行文件(若选择main.cpp
则需要用到对应的头文件路径;静态文件需要用到对应的静态库路径和静态文件名称,如-lxxx的格式)
// add.h
#ifndef ADD_H
#define ADD_H
int add(int, int);
#endif // !ADD_H
// add.cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
// minus.h
#ifndef MINUS_H
#define MINUS_H
int minus(int, int);
#endif // !MINUS_H
// minus.cpp
#include "minus.h"
int minus(int a, int b)
{
return a - b;
}
// main.cpp
#include<iostream>
#include "add.h"
#include "minus.h"
int main()
{
int a = 11;
int b = 12;
int add_res = add(a, b);
int minus_res = minus(a, b);
std::cout << "a + b = " << add_res << "\n";
std::cout << "a - b = " << minus_res << "\n";
return 0;
}
Makefile
# 目标:编译链接静态库
# 1.先找到除了main函数的源文件
cpp_srcs := $(filter-out src/main.cpp,$(shell find src -name *.cpp))
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))
# 设置头文件路径
inlcude_paths := ./include
I_options := $(inlcude_paths:%=-I%)
compile_options := -std=c++11 -g -w -O3 $(I_options)
# 然后编译为目标文件
objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@ $(compile_options)
# 编译为静态文件
lib/liboprator.a : $(cpp_objs)
@mkdir -p $(dir $@)
@ar -cr $@ $^
static_lib : lib/liboprator.a
lib_paths := ./lib/liboprator.a
# 链接文件
# 需要静态库名、静态库路径
lib_paths := ./lib
L_options := $(lib_paths:%=-L%)
lib_name := $(subst lib,,$(basename $(notdir $(shell find lib -name *.a)))) # 提取静态库名称oprator
l_options := $(lib_name:%=-l%) # 加上选项-loprator
linking_flags := $(L_options) $(l_options) # 链接选项-loprator -L./lib
bin/exec : src/main.cpp
@mkdir $(dir $@)
@g++ $< $(compile_options) -o $@ $(linking_flags)
run : bin/exec # 执行可执行文件exec
@./$<
debug :
@echo $(cpp_srcs)
@echo $(cpp_objs)
@echo $(I_options)
@echo $(compile_options)
@echo $(L_options)
@echo $(l_options)
@echo $(linking_flags)
.PHONY : debug static_lib run
# debug测试输出字符串
make debug
src/add.cpp src/minus.cpp
objs/add.o objs/minus.o
-I./include
-std=c++11 -g -w -O3 -I./include
-L./lib
-loprator
-L./lib -loprator
# run执行可执行文件
make run
a + b = 23
a - b = -1
示例4 动态库的编译
主要流程:
- 编译成动态库文件
- 源文件.cpp编译为目标文件.o,需要用到头文件路径及选项
- 编译除了main.o以外的.o文件为动态库文件.so
- 链接成可执行文件
- 将main.o与动态库链接生成可执行文件,需要用到动态库路径和选项
目录结构:
Test4
-src
-main.cpp
-add.cpp
-minus.cpp
-inlude
-add.h
-minus.h
Makefile
- 将main.o与动态库链接生成可执行文件,需要用到动态库路径和选项
// add.h
#ifndef ADD_H
#define ADD_H
int add(int, int);
#endif // !ADD_H
// add.cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
// minus.cpp
#include "minus.h"
int minus(int a, int b)
{
return a - b;
}
// minus.h
#ifndef MINUS_H
#define MINUS_H
int minus(int, int);
#endif // !MINUS_H
// main.cpp
#include<iostream>
#include "add.h"
#include "minus.h"
int main()
{
int a = 11;
int b = 12;
int add_res = add(a, b);
int minus_res = minus(a, b);
std::cout << "a + b = " << add_res << "\n";
std::cout << "a - b = " << minus_res << "\n";
return 0;
}
Makefile
# 编译动态库
# 头文件库的路径
inlcude_paths := ./include
I_options := $(inlcude_paths:%=-I%)
# 头文件编译选项
compile_options := -g -O3 -w -std=c++11 -fPIC $(I_options)
# 编译目标文件
cpp_srcs := $(shell find src -name *.cpp) # 源文件路径
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs)) # 目标文件路径
objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@ $(compile_options)
compile_objs : $(cpp_objs)
# 生成动态库
so_objs := $(filter-out objs/main.o,$(cpp_objs)) # 将需要的.o文件提取出来
lib/libop.so : $(so_objs)
@mkdir -p $(dir $@)
@g++ -shared $^ -o $@
so_run : lib/libop.so
# 链接动态库执行
# 设置动态库文件路径和选项
so_paths := ./lib
so_L := $(so_paths:%=-L%)
so_name := op
so_l := $(so_name:%=-l%)
r_options := $(so_paths:%=-Wl,rpath=%)
linking_flags := $(so_L) $(so_l) $(r_options)
bin/exec : objs/main.o compile_objs so_run
@mkdir -p $(dir $@)
@g++ $< -o $@ $(linking_flags)
run : bin/exec
@./$<
debug :
@echo $(compile_options)
@echo $(cpp_srcs)
@echo $(cpp_objs)
@echo $(linking_flags)
.PHONY : debug compile_objs
make debug
# 输出
-g -O3 -w -std=c++11 -fPIC -I./include
src/main.cpp src/add.cpp src/minus.cpp
objs/main.o objs/add.o objs/minus.o
-L./lib -lop -Wl,rpath=./lib
make run
# 输出
a + b = 23
a - b = -1