1、Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。其目的就是调高开发效率。
2、环境准备
Ubuntu、make
查看make版本命令:make -version
3、makefile文件
makefile文件由目标、依赖和命令三部分构造成,其中,makefile中还提供了变量、函数等。我们在学习的过程中要特别注意变量、函数、多目录和库生成等部分。
1、工作原理图
我们回想一下在Linux下编译程序时是不是需要指定编译器和编译参数,最麻烦的还是要将编译的源文件指出来,如果要编译的源文件不多影响倒不大,但在实际工程中的使用中,要编译的源文件就很多了,在编译时候指明就太不现实了,而且我们在修改了部分文件后,还需要重新开始编译,某些已经编译好的文件及时没有改动也需要重新编译,浪费了大量的开发时间。所以我们需要通过makefile文件解决,makefile文件有一个好处就是再次编译时只会编译发生改变的源文件,这就非常符合我们的开发需求了。
2、makefile文件时如何找到发生改变的源文件的
makefile在查找源文件的时候类似于递归,当查到第一个文件时,如果这个文件需要依赖其他源文件,就会先去判断源文件是否发生改变,这个过程很像个多叉树
3、如何使用makefile输出“Hello Word”
makefile中提供了echo,这个就类似于printf和cout,是将内容打印到终端上面的,那我们可以试着运行一下下面这段程序
target : all
@echo "Hello world!"
make 命令的执行必须到Makefile文件目录下才可以,而且需要特别注意的是,“@echo” 前面的是Tab键,不是空格键哦,我印象中通过vim打开Makefile文件时需要去配置一下vim的属性,将Tab键设置成两个空格。
我们需要注意的是,make默认编译Makefile的第一个目标,如果不指明生成目标名的话。通过上面简单的文件我们也可以看出来,就是在执行make命令的时候必须指明生成目标名, 如果不指明就默认第一个生成目标。上面的打印就是Makefile三要素中的命令,但是实际使用中,生成目标是需要多条指令的。
1、我们首先看下这个makefile文件
cc:test.o
@echo "Hello World!"
test.o:test.cc
g++ test.cc -o test
从这个文件我们能看出,cc是最终生成的目标文件,cc的生成需要依赖test,而test的生成又需要依赖test.cc文件。这样,我们在运行make命令的时候,Makefile会生成cc,但因为cc依赖test,那就需要先生产test,而test又依赖test.cc ,所以我们需要首先执行命令去生成目标test,然后才能根据test得到cc。这样我们对于目标、依赖和命令就有比较直观的理解了。
2、假目标
我们在实际书写Makefile不小心会在目录中建立一个与Makefile中同名的文件,这样我们在编译的时候去找这个文件的时候总会提示文件是最新的,这时候我们就需要通过Makefile中的(.PHONY)解决了。
.PHONY cc clean
cc:test.o
@echo "Hello World!"
test.o:test.cc
g++ test.cc -o test
clean:
rm -rf cc main test.o
● @ 用于表示一个规则中的目标。当我们的一个规则中有多个目标时, @用于表示一个规则中的目标。当我们的一个规则中有多个目标时, @用于表示一个规则中的目标。当我们的一个规则中有多个目标时,@所指的是其中任何造成命令被运行的目标。
● $^则表示的是规则中的所有先择条件。
● $<表示的是规则中的第一个先决条件。
.PHONY: clean
# 代指编译器
CC = gcc
# rm命令
RM = rm
# 生成目标名
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $(EXE) $(OBJS)
main.o: main.c
# $@ 代表到的是main.o,因为此处有多个目标,但最终造成下面这条命令执行的是生成test.o这个目标
# $^ 代表的是所有依赖,这里是main.c,如果有多个的话,就是这里的多个
# $< 代表的是所有依赖的第一个
$(CC) -o $@ $^
foo.o: foo.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
这里主要记住上面三种自动变量即可,变量还有递归扩展变量和简单扩展变量,上面RM = rm、EXE = simple就是属于递归扩展变量,简单扩展变量RM := rm、EXE := simple,简单变量在make编译时只会进行一次扫描替换。那也就是说我们在第一次编译的时候会将XX替换成mian.o,在后面的再编译过程中,都是以main.o存在的。
3、Makefile模板
# 假目标
.PHONY: clean
# 变量
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
# 目标名 依赖名
$(EXE): $(OBJS)
$(CC) -o $@ $^
# 目标是生成所有的.o文件
%.o: %.c
$(CC) -o $@ -c $^
4、函数
Make 里有一个叫 ‘wildcard’ 的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。这行会产生一个所有以 ‘.c’ 结尾的文件的列表,然后存入变量 SOURCES 里。当然你不需要一定要把结果存入一个变量。
另一个有用的函数是 patsubst ( patten substitude, 匹配替换的缩写)函数。它需要3个参数——第一个是一个需要匹配的式样,第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的字列。
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
# 将文件夹下所有以.c结尾的文件全部用SRCS代替
SRCS = $(wildcard *.c)
# 将SRCS中的是所有.c文件 替换为.o文件
OBJS = $(patsubst %.c, %.o, $(SRCS))
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
这块内容就介绍这么多。有兴趣的可以参考零声教育Darren老师总结的makefile实战和总结。这里就不过多介绍了。
https://www.yuque.com/docs/share/8495ea21-9fdb-4e7b-aca2-babd86751e39?#m6Cwu
我们在实际开发中都是做项目的,而在整个项目周期中,我们有时候更加关注的是如何去组织和管理这些代码。当然是用版本控制工具是可以在一定程度上完成这个需求的。但是版本控制工具更注重协作,在真正源文件的编译上并不能满足实际需求。那这时候有人就会说,那为什么不用makefile呢?
其实,我们是用到Makefile的,cmake是makefile的上层工具,它的目的正是为了产生可移植的makefile,并简化自己动手写makefile时的巨大工作量。那cmake既然是Makefile的上层工具,这也就注定了要比make编写起来要简单很多。
如果你自己动手写过makefile,你会发现,makefile通常依赖于你当前的编译平台,而且编写makefile的工作量比较大,解决依赖关系时也容易出错。
因此,我们在编译时更倾向于使用更加自动化的编译工具cmake。
1、cmake的特点
1,开放源代码,使用类BSD 许可发布。http://cmake.org/HTML/Copyright.html
2,跨平台,并可生成native 编译配置文件,在Linux/Unix 平台,生成 makefile,在苹果平台,可以生成xcode,在 Windows 平台,可以生成 MSVC 的工程文件。
3,能够管理大型项目,KDE4 就是最好的证明。
4,简化编译构建过程和编译过程。Cmake 的工具链非常简单:cmake+make。
5,高效虑,按照KDE 官方说法,CMake 构建KDE4 的 kdelibs 要比使用autotools 来构建KDE3.5.6 的 kdelibs 快40%,主要是因为 Cmake 在工具链中没有libtool。
6,可扩展,可以为cmake 编写特定功能的模块,扩充cmake 功能。
2、cmake打印“Hello World”
# 单个目录实现
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 工程,他不是执行文件名
PROJECT(Ray)
# 手动加入文件 ${变量名}} ,比如${SRC_LIST}
# 其实就是将.c文件换一个名称方便后面使用
SET(SRC_LIST main.c)
SET(SRC_LIST2 main2.c)
# MESSAGE和echo类似
MESSAGE(STATUS "THIS IS BINARY DIR " ${PROJECT_BINARY_DIR})
MESSAGE(STATUS "THIS IS SOURCE DIR " ${PROJECT_SOURCE_DIR})
# 生产执行文件名
ADD_EXECUTABLE(Ray ${SRC_LIST})
ADD_EXECUTABLE(RayCpp ${SRC_LIST2})
从上面我们可以看到基本语法:
PROJECT:表示项目名,它不是指要编译的这个项目名称,它是指编译CMakeLists.txt这个文件的项目名,或者直接简单来说,这东西可以随意起名,反正不会影响到项目的编译,可以直接理解为没有用处的标识符。除了指明工程名外,还可以指定编译语言,但一般都支持,所以可以忽略不填
SET:这个就是用另一种统一的符号表示源文件。
MESSAGE:他就是个打印输出。
PROJECT_BINARY_DIR:指的是当前工程的路径
ADD_EXECUTABLE:生成可执行程序的名字,这里还需要指明生成程序名所依赖的源文件。
还有一件事,cmake的编译方法是在CMakeLists.txt同级目录下执行cmake … 即可。
3、CMAKE语法
i、变量使用 方式取值( m a k e f i l e 是使用 {}方式取值(makefile是使用 方式取值(makefile是使用() ),但是在IF 控制语句中是直接使用变量名 ,
ii、指令(参数1 参数 2…),参数可以有多个。
a. 参数使用括弧括起,参数之间使用空格或分号分开。
以上面的ADD_EXECUTABLE 指令为例,如果存在另外一个 func.c 源文件,就要写成:
ADD_EXECUTABLE(hello main.c func.c)或者ADD_EXECUTABLE(hello main.c;func.c)
iii、指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。
上面的MESSAGE 指令我们已经用到了这条规则:
MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR})
也可以写成:
MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)
这里需要特别解释的是作为工程名的HELLO 和生成的可执行文件 hello 是没有任何关系的。还有就是
${HELLO_BINARY_DIR}
指的是当前工程目录,我们在执行可执行程序的时候,会打印出当前工程路径
iv,工程名和执行文件
hello 定义了可执行文件的文件名,你完全可以写成:ADD_EXECUTABLE(t1 main.c)编译后会生成一个t1 可执行文件。
v,关于语法的疑惑
cmake 的语法还是比较灵活而且考虑到各种情况,比如
SET(SRC_LIST main.c)也可以写成 SET(SRC_LIST “main.c”)
是没有区别的,但是假设一个源文件的文件名是 fu nc.c(文件名中间包含了空格)。
这时候就必须使用双引号,如果写成了 SET(SRC_LIST fu nc.c),就会出现错误,提示你找不到fu 文件和nc.c 文件。这种情况,就必须写成:
SET(SRC_LIST “fu nc.c”)
vi、内部构建与外部构建:
上面的例子展示的是“内部构建”,相信看到生成的临时文件比您的代码文件还要多的时候,估计这辈子你都不希望再使用内部构建。
对于cmake,内部编译上面已经演示过了,它生成了一些无法自动删除的中间文件,所以,引出了我们对外部编译的探讨,外部编译的过程如下:
首先,请清除t1 目录中除main.c CmakeLists.txt 之外的所有中间文件,最关键的是CMakeCache.txt。
在 t1 目录中建立build 目录,当然你也可以在任何地方建立build 目录,不一定必须在工程目录中。
进入 build 目录,运行cmake …(注意,…代表父目录,因为父目录存在我们需要的CMakeLists.txt,如果你在其他地方建立了build 目录,需要运行cmake <工程的全路径>),查看一下build 目录,就会发现了生成了编译需要的Makefile 以及其他的中间文件.
运行 make 构建工程,就会在当前目录(build 目录)中获得目标文件 hello。
4、cmake在管理工程中的应用
上面的程序我们可以看到会生成很多的中间文件,这和我们编译的出发点有一点相违背。因为我们只需要关注最终生成的Makefile文件即可,中间那么多文件我们大概率是用不到的,那我们能不能不生成这些东西呢。
首先我们需要做这样一些准备:
本小节的任务是让前面的Hello World 更像一个工程,我们需要作的是:
i,为工程添加一个子目录src,用来放置工程源代码;
ii,添加一个子目录doc,用来放置这个工程的文档hello.txt
iii,在工程目录添加文本文件COPYRIGHT, README;
iv,在工程目录添加一个runhello.sh 脚本,用来调用hello 二进制
v,将构建后的目标文件放入构建目录的bin 子目录;
vi,最终安装这些文件:将hello 二进制与runhello.sh 安装至/usr/bin,将doc
目录的内容以及COPYRIGHT/README 安装到/usr/share/doc/cmake/t2.
然后我们将src的上一级目录的CMakeLists.txt中添加子目录ADD_SUBDIRECTORY(src)和INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/Ray),然后去执行cmake … 和 make命令
# 指得是向当前工程添加存放源文件的子目录
ADD_SUBDIRECTORY(...) 指令
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
5、其它指令的一些使用
INCLUDE_DIRECTORIES:INCLUDE_DIRECTORIES(“${CMAKE_CURRENT_SOURCE_DIR}/dir_1”)
# 代表的是当前执行的CMakeLists.txt所在文件路径
CMAKE_CURRENT_SOURCE_DIR
# 指的是指当前正在处理的列表文件目录
CMAKE_CURRENT_LIST_DIR
# 添加子目录
ADD_SUBDIRECTORY
ADD_SUBDIRECTORY("${CMAKE_CURRENT_SOURCE_DIR}/dir_2")
# 链接库到执行文件上
TARGET_LINK_LIBRARIES
TARGET_LINK_LIBRARIES(darren dir_1 dir_2)
# 找到某个路径下的所有源文件
AUX_SOURCE_DIRECTORY
6、接下来的内容就是关于cmake编译生成动态库和静态库的方法了
生成库
# 设置release版本还是debug版本
# if语句中可以直接使用变量名
if(${CMAKE_BUILD_TYPE} MATCHES "Release")
MESSAGE(STATUS "Release版本")
SET(BuildType "Release")
else()
SET(BuildType "Debug")
MESSAGE(STATUS "Debug版本")
endif()
#设置lib库目录
# PROJECT_SOURCE_DIR:跟着最近的project文件目录
SET(RELEASE_DIR ${PROJECT_SOURCE_DIR}/release)
# debug和release版本目录不一样
#设置生成的so动态库最后输出的路径
SET(LIBRARY_OUTPUT_PATH ${RELEASE_DIR}/linux/${BuildType})
# -fPIC 动态库必须的选项
ADD_COMPILE_OPTIONS(-fPIC)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
AUX_SOURCE_DIRECTORY(. DIR_LIB_SRCS)
# 生成静态库链接库Dir1
#ADD_LIBRARY (Dir1 ${DIR_LIB_SRCS})
# 生成动态库
ADD_LIBRARY (Dir1 SHARED ${DIR_LIB_SRCS})
生成静态库并安装到指定目录
# 设置release版本还是debug版本
if(${CMAKE_BUILD_TYPE} MATCHES "Release")
MESSAGE(STATUS "Release版本")
SET(BuildType "Release")
else()
SET(BuildType "Debug")
MESSAGE(STATUS "Debug版本")
endif()
#设置lib库目录
SET(RELEASE_DIR ${PROJECT_SOURCE_DIR}/release)
# debug和release版本目录不一样
#设置生成的so动态库最后输出的路径
SET(LIBRARY_OUTPUT_PATH ${RELEASE_DIR}/linux/${BuildType})
ADD_COMPILE_OPTIONS(-fPIC)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
AUX_SOURCE_DIRECTORY(. DIR_LIB_SRCS)
# 生成静态库链接库Dir1
ADD_LIBRARY (Dir1 ${DIR_LIB_SRCS})
# 将库文件安装到lib目录
INSTALL(TARGETS Dir1 DESTINATION lib)
# 将头文件include
INSTALL(FILES dir1.h DESTINATION include)
编译安装
ubuntu% cmake -DCMAKE_INSTALL_PREFIX=/tmp/usr ..
ubuntu% make
ubuntu% make install
[100%] Built target Dir1
Install the project...
-- Install configuration: ""
-- Up-to-date: /tmp/usr/lib/libDir1.a
-- Installing: /tmp/usr/include/dir1.h
具体内容及实现参考:
http://www.mamicode.com/info-detail-2439626.html
7、调用库文件
调用静态库
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 工程
PROJECT(0VOICE)
# 手动加入文件
SET(SRC_LIST main.c)
MESSAGE(STATUS "THIS IS BINARY DIR " ${PROJECT_BINARY_DIR})
MESSAGE(STATUS "THIS IS SOURCE DIR " ${PROJECT_SOURCE_DIR})
# 头文件路径
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib")
# 库的路径
LINK_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib")
# 生成执行文件
ADD_EXECUTABLE(Ray ${SRC_LIST})
# 引用动态库
TARGET_LINK_LIBRARIES(Ray Dir1)
首先是在CMakeLists.txt所在的目录下去寻找头文件,然后LINK_DIRECTORES相当于g++、gcc编译中的-L选项,最终生成可执行程序Ray。当然我们需要指明需要连接的库名称。
调用动态库
# 单个目录实现
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 工程
PROJECT(0VOICE)
# 手动加入文件
SET(SRC_LIST main.c)
MESSAGE(STATUS "THIS IS BINARY DIR " ${PROJECT_BINARY_DIR})
MESSAGE(STATUS "THIS IS SOURCE DIR " ${PROJECT_SOURCE_DIR})
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib")
LINK_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib")
# 引用动态库
ADD_EXECUTABLE(Ray ${SRC_LIST})
#同时静态库、动态库 优先连接动态库
#TARGET_LINK_LIBRARIES(darren Dir1)
# 强制使用静态库
TARGET_LINK_LIBRARIES(Ray libDir1.a)
这里的流程大致差不多,需要注意的就是同时存在静态库、动态库时他们的调用顺序。今天的内容先这些,后期等我掌握了在继续写,写的太乱了。