makefile项目构建
OVERVIEW
- makefile项目构建
- 1.概念
- 2.make选项
- 3.makefile语法
- (1)基本语法
- (2)系统与自定变量
- (3)常用函数
- (4)模式匹配与伪目标
- 4.makefile编译流程
- (1)预处理
- (2)编译
- (3)汇编
- (4)链接
- (5)编译参数
- 5.编译动态链接库
- (1)编译动态库
- (2)链接动态库
- (3)运行时使用动态库
- 6.编译静态链接库
- (1)编译静态库
- (2)链接静态库
- (3)运行时使用静态库
- 7.make install
- GUN Make官方网站:https://www.gnu.org/software/make/
- GUN Make官方文档下载地址:https://www.gnu.org/software/make/manual/
- Makefile Tutorial:https://makefiletutorial.com/
1.概念
- make命令工具
make是一个命令工具,用于解释makefile中的指令,
- makefile文件
工程文件中的源文件不计其数,其按照类型、功能、模块分别存放在若干目录中,
makefile定义一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于更复杂的功能操作,
makefile文件中描述了整个工程所有文件的编译顺序、编译规则,
- cmake编译工具
cmake是一个跨平台安装的编译工具,可以用简单的语句描述所有平台的编译安装过程,
其能够输出各种各样的makefile或project文件,能够测试编译器所支持的C++特性,类似unix下的automake,
其并不直接构建出最终的软件,而是产生标准的构建档(如unix的makefile或windows visual c++的projects/workspaces),然后再以一般的构建方式使用,
- CMakeLists.txt文件
cmake是一个命令工具可用于生成makefile,但也要根据CMakeLists.txt中的内容来生成,而CMakeLists.txt就是写给cmake的规则,
-
总结:
make是一个命令工具,makefile是一个文件,make执行的时候需要读取makefile文件中的规则(makefile规则需要自己写),
make是一个命令工具,CMakeLists.txt是一个文件,cmake执行的时候需要读取CMakeLists.txt文件中的规则(CMakeLists.txt规则需要自己写),
2.make选项
make常用选项:make [-f file] [options] [target]
,Make默认在当前目录中寻找GUNmakefile,
- -v:显示版本号,
- -f:指定除上述文件名之外的文件作为输入文件,
- -n:只输出命令,但不执行,一般用来测试,
- -s:只执行命令,但不显示具体命令,此处可在命令中用
@
符抑制命令输出, - -w:显示执行前执行后的路径,
- -C dir:指定makefile所在的目录,
3.makefile语法
GCC ?= gcc
CCMODE = PROGRAM
INCLUDES = -I include
# CFLAGS = -Wall $(MACRO)
TARGET = test.out
SRCS := $(wildcard src/*.cpp)
# LIBS = -lpthread -lncurses
ifeq ($(CCMODE),PROGRAM)
$(TARGET): $(LINKS) $(SRCS)
$(GCC) $(CFLAGS) $(INCLUDES) -o $(TARGET) $(SRCS) $(LIBS)
@chmod +x $(TARGET)
@echo make $(TARGET) ok.
clean:
rm -rf $(TARGET)
endif
(1)基本语法
[target]:[prerequisties]
[command]
- 目标TARGET:可以是需要进行编译的目标,也可以是一个动作(目标文件OBJECTFile、可执行文件、标签Label)
- 依赖DEPENDE:执行当前目标所要依赖的选项,包括其他目标,某个具体文件或库等,
- 命令command:该目标下要执行的具体命令,
makefile中都是先展开所有的变量,再调用指令进行相关的操作,
=
:赋值操作,使用终值进行赋值操作,不论变量调用写在赋值前\后,调用时都是取最终值,:=
:赋值操作,只受当前行以及之前的代码的影响,而不会收到后面的赋值的影响,?=
:默认赋值运算符,如果该变量已经定义则不进行任何操作、如果该变量尚未定义则求值并进行分配,
(2)系统与自定变量
变量在声明时需要赋予初值,而在使用时需要在变量名前加上 $
符号,并使用小括号将变量括起来,
- 系统变量:
- $*:不包括扩展名的目标文件名称
- $+:所有的依赖文件,以空格进行分割
- $<:表示规则中的一个条件
- $?:所有时间戳比目标文件晚的依赖文件,以空格分隔,
- $@:目标文件的完整名称
- $^:所有不重复的依赖文件,以空格进行分隔
- $%:如果目标是归档成员,则该变量表示目标的归档成员名称,
- 系统常量,
make -p
进行查看:- AS:
as
,汇编程序的名称 - CC:
cc
,C编译器名称 - CPP:
cc -E
,C预编译器名称 - CXX:
g++
,C++编译器名称 - RM:
rm -f
,文件删除别名
- AS:
- 自定义变量:
- 定义:变量名 = 变量值
- 使用:
$(变量名)/${变量名}
# version1
# 分开书写 保证只编译有改动的代码
calc:add.o sub.o multi.o divide.o calc.o
gcc add.o sub.o multi.o divide.o calc.o -o calc
add.o:add.cpp
gcc -c add.cpp -o add.o
sub.o:sub.cpp
gcc -c sub.cpp -o sub.o
multi.o:multi.cpp
gcc -c multi.cpp -o multi.o
divide.o:divide.cpp
gcc -c divide.cpp -o divide.o
calc.o:calc.cpp
gcc -c calc.cpp -o calc.o
clean:
rm -rf *.o calc
# version2
TARGET = calc
OBJ = add.o sub.o multi.o divide.o calc.o
$(TARGET):$(OBJ)
gcc $(OBJ) -o $(TARGET)
add.o:add.cpp
gcc -c add.cpp -o add.o
sub.o:sub.cpp
gcc -c sub.cpp -o sub.o
multi.o:multi.cpp
gcc -c multi.cpp -o multi.o
divide.o:divide.cpp
gcc -c divide.cpp -o divide.o
calc.o:calc.cpp
gcc -c calc.cpp -o calc.o
clean:
rm -rf *.o calc
# version3
TARGET = calc
OBJ = add.o sub.o multi.o divide.o calc.o
$(TARGET):$(OBJ)
$(CXX) $^ -o $@
add.o:add.cpp
$(CXX) -c $^ -o $@
sub.o:sub.cpp
$(CXX) -c $^ -o $@
multi.o:multi.cpp
$(CXX) -c $^ -o $@
divide.o:divide.cpp
$(CXX) -c $^ -o $@
calc.o:calc.cpp
$(CXX) -c $^ -o $@
clean:
$(RM) *.o $(TARGET)
(3)常用函数
函数调用与变量使用类似,也是以 $
来标识的,其基本语法如下:
# fn 函数名 arguments 函数参数
$(fn, arguments) or ${fn, arguments}
-
shell函数:
$(shell <command> <arguments>)
调用shell命令command,
函数返回shell命令command的执行结果,
cpp_srcs:=$(shell find src -name "*.cpp")
-
subst函数:
$(subst <from>, <to>, <text>)
字符串替换函数subst,将字符串
<text>
中的<from>
字符串替换成为<to>
函数返回被替换后的字符串结果,
-
patsubst函数:
$(patsubst <pattern>, <replacement>, <text>)
模式字符串替换函数,从text中提取出pattern,替换成replacement,
函数返回被替换过后的字符串,
-
foreach函数:
$(foreach <var>, <list>, <text>)
循环函数,将字串
<list>
中的元素逐个取出,执行<text>
包含的表达式,include_paths:= /usr/include \ /usr/include/opencv2/core \ /usr/include/mysql \ /usr/include/redis include_paths:=$(foreach item, $(include_paths), -I $(item)) debug: echo $(include_paths)
include_paths:= /usr/include \ /usr/include/opencv2/core \ /usr/include/mysql \ /usr/include/redis include_paths:= $(include_paths:%=-I%) debug: echo $(include_paths)
-
dir函数:
$(dir <names...>)
取目录函数,从文件名序列中取出目录部分,目录部分是指最后一个反斜杠
/
之前的部分(如果没有反斜杠则直接返回./
),函数返回文件名序列的目录部分
-
notdir、filter、basename函数,
libs := $(shell find /usr/lib -name lib*) libs := $(notdir $(libs)) a_libs := $(filter %.a, $(libs)) so_libs := $(filter %.so, $(libs)) libs := $(basename $(libs)) libs := $(subst lib, , $(libs)) debug: echo $(libs) # echo $(a_libs) # echo $(so_libs)
(4)模式匹配与伪目标
伪目标:.PHONY:clean
,声明伪目标之后,makefile将不会判断目标是否存在 或 该目标是否需要进行更新,
模式匹配:%目标:%依赖
,
%.o:%.cpp
:.0
依赖于对应的.cpp
文件,wildcard
:$(wildcard ./*.cpp)
获取当前目录下所有的.cpp
文件patsubst
:$(patsubst %.cpp, %.o, ./*.cpp)
将对应的cpp文件名替换成为.o
文件名,
# version4
.PHONY:clean
TARGET = calc
OBJ = $(patsubst %.cpp, %.o, $(wildcard ./*.cpp))
# OBJ = add.o sub.o multi.o divide.o calc.o
$(TARGET):$(OBJ)
$(CXX) $^ -o $@
%.o:%.cpp
$(CXX) -c $^ -o $@
clean:
$(RM) *.o $(TARGET)
4.makefile编译流程
GCC是GUN编译程序集合(GUN Compile Collection),是最重要的开放源码软件,
其他所有开放源码软件都在某种层次上依赖它,甚至其他语言如python、都是由C语言开发的,由GUN编译程序编译的。
GCC包含的常见的软件,
- ar:通过从文档中添加、删除和析取文件来维护库文件。通常使用该工具是为了创建和管理,连接程序使用的目标库文件,
- as:GUN汇编器,实际上是一族汇编器,因为可以被编译或能够在各种不同的平台上工作,
- gdb:GUN调试器,用于检测程序运行时的值和行为GNATS:GUN调试跟踪系统,
- gprof:监督编译程序的执行过程,并报告程序中各个函数的运行时间,可以根据所提供的配置文件来优化程序,
- ld:GUN连接程序,该程序将目标文件的集合组合成可执行程序,
- libtool:一个基本库,支持make程序的描述文件使用的简化共享库用法的脚本,
- make:一个工具程序,其会读取makefile脚本来确定程序中的哪个部分需要编译和连接,然后执行必要的命令,
- 查看GCC默认头文件搜索路径:
echo | gcc -v -x c -E -
#include <iostream>
using namespace std;
int main() {
cout << "this is a test!~" << endl;
return 0;
}
gcc -lstdc++ main.cpp,直接从源代码到可执行文件,将上述过程进行拆分为:预处理、编译、汇编、链接操作,
(1)预处理
预处理:gcc -E main.cpp>main.i
预处理
预处理后得到main.i文件,
# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "/usr/include/c++/7/iostream" 1 3
# 36 "/usr/include/c++/7/iostream" 3
# 37 "/usr/include/c++/7/iostream" 3
# 1 "/usr/include/x86_64-linux-gnu/c++/7/bits/c++config.h" 1 3
# 229 "/usr/include/x86_64-linux-gnu/c++/7/bits/c++config.h" 3
# 229 "/usr/include/x86_64-linux-gnu/c++/7/bits/c++config.h" 3
namespace std
{
typedef long unsigned int size_t;
typedef long int ptrdiff_t;
typedef decltype(nullptr) nullptr_t;
}
......
namespace std __attribute__ ((__visibility__ ("default")))
{
# 60 "/usr/include/c++/7/iostream" 3
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;
static ios_base::Init __ioinit;
}
# 9 "main.cpp" 2
# 9 "main.cpp"
using namespace std;
int main() {
cout << "this is a test!~" << endl;
return 0;
}
(2)编译
编译:gcc -S main.i
编译后得到 main.s文件
.file "main.cpp"
.text
.section .rodata
.type _ZStL19piecewise_construct, @object
.size _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
.zero 1
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.LC0:
.string "this is a test!~"
.text
.globl main
.type main, @function
main:
.LFB1494:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rsi
leaq _ZSt4cout(%rip), %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq %rax, %rdx
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax
movq %rax, %rsi
movq %rdx, %rdi
call _ZNSolsEPFRSoS_E@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1494:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB1983:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L5
cmpl $65535, -8(%rbp)
jne .L5
leaq _ZStL8__ioinit(%rip), %rdi
call _ZNSt8ios_base4InitC1Ev@PLT
leaq __dso_handle(%rip), %rdx
leaq _ZStL8__ioinit(%rip), %rsi
movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
movq %rax, %rdi
call __cxa_atexit@PLT
.L5:
nop
leave
.cfi_def_cfa 7, 8
ret
(3)汇编
汇编:gcc -c main.s
汇编之后得到二进制文件 main.o(.obj)
(4)链接
链接:gcc -lstdc++ main.o -o main.out
得到可执行文件 main.out
(5)编译参数
编译选项:
-
-m64
:指定编译为 64/32 位应用程序 -
-std=
:指定编译标准,例如:-std=c++11、-std=c++14 -
-g
:包含调试信息 -
-w
:不显示警告 -
-O
:优化等级,通常使用:-O 3 -
-I
:加在头文件路径前 -
-fPIC
:Position-Independent Code,产生的没有绝对地址,全部使用相对地址,
代码可以被加载到内存的任意位置,并且可以被正确执行,
这正是共享库所需要的,共享库被加载时,在内存的位置不是固定的,
链接选项:
-l
:加在库名前面-L
:加在库路径前面-Wl, <选项>
:将逗号分隔的<选项>
传递给链接器-rpath=
:运行的时候去找的目录,要找找到.so
文件,会从这个选项里指定的地方去找,
约定的变量名称:
- CC:Program for compiling C programs
- CXX:Program for compiling C++ programs
- CFLAGS:Extra flags to give to the C compiler
- CXXFLAGS:Extra flags to give to the C++ compiler
- CPPFLAGS:Extra flags to give to the C preprocessor
- LDFLAGS:Extra flags to give to compilers when they are supposed to invoke the linker
5.编译动态链接库
动态链接库:不会把代码编译到二级制文件中,而是在运行时才去加载,所以只需要维护一个地址即可,
动态库编译完成之后需要发布,否则程序运行时找不到,
windows环境下动态库为.dll、linux环境下动态库为.so
-
编译成
.o
文件:g++ -c -fpic soTest.cpp -o soTest.o
-
编译动态库:
g++ -shared soTest.o -o libsoTest.so
- -c:得到二进制文件aTest.o
- -shared:共享
- -fPIC:产生位置无关的代码,
- -l:小写l,指定动态库,
- -L:手动指定库文件搜索目录,默认只链接共享目录,
- -I:大写i,指定头文件目录(默认当前目录),
-
链接成执行文件:
g++ [.cpp] -l [libName] -L [libPath] -o [test.out]
g++ soTest.cpp -shared -fPIC -o libsoTest.so
(1)编译动态库
文件目录结构如下,将其打包成动态库,
// soTest.h
#ifndef _SOTEST_H
#define _SOTEST_H
#include <iostream>
using namespace std;
class soTest {
public:
void func1();
virtual void func2();
virtual void func3() = 0;
};
#endif
// soTest.cpp
#include "soTest.h"
void soTest::func1()
{
cout << "this is func1" << endl;
}
void soTest::func2()
{
cout << "this is func2" << endl;
}
# makefile
libsoTest.so:
$(CXX) soTest.cpp -shared -fPIC -L ./ -o libsoTest.so
clean:
$(RM) libsoTest.so
使用make libsoTest.so 命令成功完成对 libsoTest.so
动态库的打包操作,
(2)链接动态库
在动态库成功打包出来之后,在其他项目中通过引入 soTest.h
与 libsoTest.so
文件,来使用打包好的动态库,
文件目录结构如下,将第三方动态库动态载入,编译自己的项目,
//test.cpp
#include <iostream>
#include "soTest.h"
using namespace std;
class Test:public soTest{
public:
void func2() {
cout << "Test:this is func2" << endl;
}
void func3() {
cout << "Test:this is func3" << endl;
}
};
int main() {
Test t1;
t1.func1();
t1.func2();
t1.func3();
return 0;
}
# makefile
test:
$(CXX) test.cpp -lsoTest -L ./ -I ./ -o test.out
clean:
$(RM) *.out
使用make test 命令成功完成第三方动态库的链接,编译成功目录下出现 test.out
的可执行文件,
(3)运行时使用动态库
由于动态库的特点,若只在编译时使用的动态库,而运行时没有指定动态库位置,则程序将无法正常运行,
即动态库编译完成之后需要进行发布操作,否则程序运行时会找不到动态库位置而产生报错,如下所示:
./a.out: error while loading shared libraries: libsoTest.so: cannot open shared object file: No such file or directory
-
解决方案1:将动态库so文件拷贝到对应的目录下(发布),才能运行程序
-
linux下默认动态库路径配置文件:
/etc/ld.so.conf
、/etc/ld.so.conf.d/*.conf
-
/usr/lib
-
/usr/local/lib
-
-
解决方案2:运行时手动指定动态库的所在目录
mac环境:
DYLD_LIBARY_PATH=./your_lib_path
export DYLD_LIBARY_PATH
linux环境:
LD_LIBARY_PATH=./your_lib_path
export LD_LIBARY_PATH
6.编译静态链接库
静态链接库:会将库中的代码编译到二进制文件中,当程序编译完成后,该库文件可以删除,
与静态库不同的是,动态链接库必须与程序同时部署,还要保证程序能正常加载得到的库文件。静态库可以不用部署已经加载到程序中,而且运行时的速度更快,
但是会导致程序体积更大,并且库中的内容如果有更新,则需要重新编译生成程序,
windows环境下动态库为.lib、linux环境下动态库为.a
-
编译成
.o
文件:g++ -c aTest.cpp -o aTest.o
-
编译静态库:
ar -r libaTest.a aTest.o
- -c:得到二进制文件aTest.o
- ar:备份压缩命令,将目标文件打包成静态链接库,
- -r:将文件插入备存文件中,
-
链接成执行文件:
g++ [.cpp] [.a] -o [test.out]
g++ [.cpp] -l [libName] -L [libPath] -o [test.out]
(1)编译静态库
文件目录结构如下,将其打包成静态库,
// aTest.h
#ifndef _ATEST_H
#define _ATEST_H
#include<iostream>
using namespace std;
class aTest{
public:
void func1();
};
#endif
// aTest.cpp
#include "aTest.h"
void aTest::func1()
{
cout << "aTest:func1" << endl;
}
# makefile
libaTest.a:
$(CXX) -c aTest.cpp -L ./ -I ./ -o aTest.o
$(AR) -r libaTest.a aTest.o
clean:
$(RM) *.a *.o
使用make libaTest.a 命令成功完成对 libaTest.a
静态库的打包操作,
(2)链接静态库
在静态库成功打包出来之后,在其他项目中通过引入 aTest.h
与 libaTest.a
文件,来使用打包好的静态库,
文件目录结构如下,将第三方静态库动态载入,编译自己的项目,
// test.cpp
#include <iostream>
#include "aTest.h"
using namespace std;
int main() {
aTest t1;
t1.func1();
return 0;
}
# makefile
test:
$(CXX) test.cpp -laTest -L ./ -I ./ -o test.out
clean:
$(RM) *.out
使用make test 命令成功完成第三方静态库的链接,编译成功目录下出现 test.out
的可执行文件,
(3)运行时使用静态库
由于静态库的特点,在编译时已经将库中的代码编译到二进制文件中,当编译完成后,该库文件可以删除,并且程序可以直接运行,
7.make install
-
make,编译链接:
将源文件,编译成二进制的可执行文件(包括各种库文件)
-
make install,配置相关的运行环境:
创建目录,将可执行文件拷贝到指定目录(安装目录)
加全局可执行的路径
加全局的启动停止脚本
-
make clean
重置编译环境,删除无关文件
TARGET:=my_test
OBJ:=$(TARGET).os
CC:=g++
PATHS:=/tmp/test/
BIN:=/usr/local/bin/
START_SH:=$(PATHS)$(TARGET)
STOP_SH:=$(PATHS)$(TARGET)
$(TARGET):$(OBJ)
install:$(TARGET)
if [ -d $(PATHS) ]; \
then echo $(PATHS) exist; \
else \
mkdir $(PATHS); \
cp $(TARGET) $(PATHS); \
ln -sv $(PATHS)$(TARGET) $(BIN); \
touch $(LOG); \
chmod a+rwx $(LOG); \
echo "$(TARGET)>$(LOG) & echo $(TARGET) running...">$(PATHS)$(START_SH); \
echo "killall $(TARGET)">$(PATHS)$(START_SH); \
chmod a+x $(PATHS)$(START_SH); \
chmod a+x $(PATHS)$(STOP_SH); \
ln -sv $(PATHS)$(START_SH) $(BIN); \
ln -sv $(PATHS)$(STOP_SH) $(BIN); \
fi;
clean:
$(RM) $(TARGET) $(OBJ) $(BIN)$(TARGET) $(BIN)$(START_SH) $(BIN)$(STOP_SH)
$(RM) -rf $(PATHS)
.PHONY:clean install