前面文章介绍了C++编译过程:预处理、编译、汇编、链接,内容比较简单,只要会使用命令行,就能根据文章的内容实践操作,直观的了解编译全过程。
一个项目往往不只一两个cpp文件,此时命令行编译的方式就会显得捉襟见肘。然而在实际项目中,有序构建并不需要开发人员投入太多精力,这就必须要用到编译脚本,了解一两个常用命令,就可以搞定这个复杂的过程。
如果要说什么是C++开发中可以真正做到一劳永逸的事情,那就是编译脚本。编译脚本是一个规则描述文件,配合工具就可以完成编译,其维护成本极低,只需要参考模板简单改改,就可以拿来直接用,岂不美哉。
make与Makefile
make是上个世纪70年代诞生的工具,能够沿用至今,必属经典。
大型工程的编译面临依赖多、耗时长的问题,有时候你只改了一小段代码,却要等好几十秒甚至几分钟来等待编译结果。在企业级的中心化构建系统中,还可能会存在队列等待的问题。
其实大型工程也是基于一些基础的小工程构建的,将大型工程用可插拔的理念,拆分成一个个小单元,那么在这些小的单元中,其实用不到太复杂的构建系统,选择make可以快速验证demo程序是否符合预期。没有RPC、网络、权限校验、流程等诸多工程问题。
make指令需要一个构建规则,这个规则默认放在Makefile中,当然也可以通过-f参数指定规则文件。话不多说,直接上菜:
$(shell if [ ! -d sbin ]; then mkdir sbin; fi;)
TAR := ./sbin/cppcompiletemplate
CXX := g++ # -m32
SOURCES :=$(wildcard *.cpp) $(wildcard *.c) $(wildcard *.hpp) $(wildcard *.cc)
AllDirs := $(shell ls -R | grep '^\./.*:$$' | awk '{gsub(":","");print}' | grep -vE 'cmakebuild|sbin')
$(info AllDirs: $(AllDirs))
SOURCES += $(foreach subdir,$(AllDirs),$(wildcard $(subdir)/*.cpp) $(wildcard $(subdir)/*.c)) # 遍历子目录
COPTION := -W -Wall -Wfatal-errors -fpermissive # -Werror -Wshadow -Wdouble-promotion -fno-common -Wconversion
#COPTION += -Wno-unused-function -Wno-error=missing-field-initializers
CFLAGS := -pthread -std=c++11 $(COPTION)
CFLAGS_DEBUG := -g -O0
CFLAGS_RELEASE := -s -O2 -static-libstdc++ -static-libgcc # -static
###############################################################################################################
DEFS := #-DNDEBUG
CFLAGS += # -pg
INC := # -I ./3rdlib/jsoncpp/include
LIB := # ./3rdlib/jsoncpp/lib/libjsoncpp.a
LFLAGS := -pthread #-lrt -ldl
###############################################################################################################
all: $(TAR)
debug: $(TAR)
$(TAR): $(SOURCES)
@echo -e =====================DEBUG=========================
$(CXX) $(DEFS) $(CFLAGS) $(CFLAGS_DEBUG) -o $(TAR) $(SOURCES) $(INC) $(LIB) $(LFLAGS)
release: $(SOURCES)
@echo -e =====================RELEASE=======================
$(CXX) $(DEFS) $(CFLAGS) $(CFLAGS_RELEASE) -o $(TAR) $(SOURCES) $(INC) $(LIB) $(LFLAGS)
%.o: %.cpp
@echo -e ======================COMPLING====================
@echo Compling $< "->" $@
@echo -e ==================================================
$(CXX) $(CFLAGS) -c $*.cpp $(INC)
clean:
rm -f *.o
rm -f $(TAR)
Makefile是通过变量的方式来组织编译规则。这没什么难的,熟悉命令行的基本就能看懂。使用方法:
- 拷贝文件内容到工程根目录Makefile文件中
- 执行make指令,会扫描当前目录与子目录下的所有c++源文件进行编译
通常,使用时只需要修改#注释包裹的内容。这是我使用得最多的模板,可适用于绝大部分单体服务工程。
注意:这里我注释掉了-static链接选项,实际工程中一般不要使用-static静态链接,详细原因我在《Linux C++生成静态库与动态库》中有说明。
cmake与CMakeLists.txt
make虽然简单方便,但是可移植性不足。比如上面的Makefile第一行命令(生成sbin目录)和最后一行命令(清除可执行文件),在windows下是行不通的,cmake就可以完美解决这个问题。
cmake诞生于2000年,比make晚了23年。cmake本身不直接产生可执行文件,而是用来生成标准的Makefile规则描述文件或开发工程。
如果你去看github上的开源库,可以发现绝大部分库都使用cmake做为编译工具,其原因一是可以跨平台,二是cmake有生态的理念,内置了很多宏,可以快速引入其他依赖包,如果你编译过grpc就可以深刻体会到这点。继续上菜:
cmake_minimum_required(VERSION 2.8) # 指定cmake的最小版本
cmake_policy(SET CMP0015 NEW) # 解决Policy CMP0015 is not set错误
set(DEMO_NAME cppcompiletemplate)
set(PROJECT_SOURCE_DIR ".")
set(EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/sbin") # 指定可执行程序编译输出目录
set(LIBRARY_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/lib") # 指定静态库或者动态库编译输出目录
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_C_COMPILER ${CMAKE_CXX_COMPILER}) # 使用g++来编译.c文件,否则c的函数就需要extern "C"导出
# 自动检测编译器是否支持C++11
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
if(COMPILER_SUPPORTS_CXX11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
message(WARNING "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()
# add_compile_options命令添加的编译选项是针对所有编译器的(包括c和c++编译器)
add_compile_options(-W -Wall -Wfatal-errors -fpermissive) # add_compile_options在末尾添加参数,不能加双引号
set(LFLAGS_DEBUG "-O0 -g") # Debug模式
set(LFLAGS_RELEASE "-O2 -s -static-libstdc++ -static-libgcc") # Release模式
# set命令设置CMAKE_C_FLAGS或CMAKE_CXX_FLAGS变量则是分别只针对c和c++编译器的
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${LFLAGS_DEBUG}") # 使用 cmake -DCMAKE_BUILD_TYPE=Debug ../
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${LFLAGS_DEBUG}")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${LFLAGS_RELEASE}") # 使用 cmake -DCMAKE_BUILD_TYPE=Release ../
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${LFLAGS_RELEASE}")
###############################################################################################################
add_definitions() # 额外的宏定义
add_compile_options() # 额外的编译选项
set(SRC_DIR_ROOT ) # 设置源文件目录,这里不能用双引号
include_directories() # 设置包含目录
link_directories() # 设置动态和静态链接库搜索目录
link_libraries() # 静态链接link_libraries用在add_executable之前,需要结合link_directories使用
###############################################################################################################
include_directories(./ ${SRC_DIR_ROOT})
FOREACH(SRC_DIR ${SRC_DIR_ROOT})
file(GLOB_RECURSE USER_FILE_PATH ${USER_FILE_PATH} ${SRC_DIR}/*.cc ${SRC_DIR}/*.cpp ${SRC_DIR}/*.hpp)
ENDFOREACH(SRC_DIR)
aux_source_directory(. SRC_LIST) # 编译源文件
add_executable(${DEMO_NAME} ${SRC_LIST} ${USER_FILE_PATH}) # 编译可执行程序
#target_link_libraries(${DEMO_NAME} rt) # 动态链接target_link_libraries用在add_executable之后,需要结合link_directories使用
# -pthread既是编译指令,也是链接指令。编译只能指定-pthread,也可以不指定。
# 链接时:-pthread参数最终是-pthread指令,pthread是参数会转换成-lpthread,和直接指定-lpthread一样的效果。
# set_target_properties一次性强制指定编译和链接-pthread,需要放到add_executable之后
set_target_properties(${DEMO_NAME} PROPERTIES COMPILE_FLAGS "-pthread" LINK_FLAGS "-pthread")
message(STATUS "${SRC_LIST} ${USER_FILE_PATH}")
#add_library(${DEMO_NAME} STATIC ${SRC_LIST}) # 编译静态库
#add_library(${DEMO_NAME} SHARED ${SRC_LIST}) # 编译动态库
make使用变量组织编译规则,cmake在make的基础上增加了函数的概念。但是cmake的使用文档晦涩难懂,所以大部分内容我都写了注释。使用方法:
- 拷贝文件内容到工程根目录CMakeLists.txt中
- 创建cmakebuild子目录,并进入cmakebuild目录
- 生成Debug模式Makefile:cmake -DCMAKE_BUILD_TYPE=Debug ..
- 生成Release模式Makefile:cmake -DCMAKE_BUILD_TYPE=Release ..
大部分情况下,使用时可以像使用Makefile那样,只需要修改#注释包裹的内容。如果需要引入第三方库,参考如下:
include_directories(./3rdlib/jsoncpp/include) # 设置包含目录
link_directories(./3rdlib/jsoncpp/lib) # 设置动态和静态链接库搜索目录
link_libraries(libjsoncpp.a)
CMakeLists.txt主要通过函数的方式来组织编译规则,需要提醒的是在一些大型工程中,比如folly和mysql,它们的CMakeLists.txt会使用正则表达式MATCHES来匹配文件路径,而"+"号在正则表达式中具有特殊含义,因此这些工程不能放在类似于/C++/这样的路径下。在modern cmake(3.x版本)中还引入对象的概念,然而对象方式组织编译最好最清晰的当属分布式编译工具bazel。
bazel与BUILD
bazel是google推出的构建工具,其社区非常活跃,推荐使用bazelisk安装bazel工具。其编译规则描述文件是BUILD,编写方式和python语法类似。
cc_import(
name = 'libjsoncpp',
hdrs = glob([
'3rdlib/jsoncpp/include/**/*.h',
]),
static_library = '3rdlib/jsoncpp/lib/libjsoncpp.a',
)
cc_library(
name = 'cppcompiletemplate_comm',
srcs = glob([
'*.cpp',
'*.cc',
'*.hpp',
],
exclude = ['main.cpp'],
),
hdrs = glob(['*.h']),
includes =['.'],
deps = [
#':libjsoncpp',
],
copts = [
'-W',
'-Wall',
'-Werror',
'--std=c++11',
],
linkopts = [
'-pthread',
]
)
cc_binary(
name = 'cppcompiletemplate',
srcs = ['main.cpp'],
includes =['.'],
deps = [
':cppcompiletemplate_comm',
],
copts = [
'-W',
'-Wall',
'-Werror',
'--std=c++11',
],
linkopts = []
)
是不是很简单,
- cc_import()导入第三方库(如果需要)
- cc_library()将除main.cpp以外的文件封装成静态库对象,可以再拆分成多个cc_library对象,方便其他工程依赖
- cc_binary()编译成最终的可执行文件
使用方法:
- 拷贝文件到工程根目录
- 在仓库的根目录下创建一个WORKSPACE文件
- 执行bazel build :cppcompiletemplate生成二进制,同理,执行bazel build :cppcompiletemplate_comm则生成lib库。
生成的文件在WORKSPACE文件所在目录下的bazel-out软链接的目录中。
bazel按对象来组织编译规则的理念,让依赖看起来非常清晰,而且其缓存机制,使得在大型工程中仍然有较快的构建速度,嘎嘎好。
总结
- make的Makefile使用变量来描述编译规则,经典范式,简单易懂,适合小demo;
- cmake的CMakeLists.txt通过变量、函数来描述编译规则,跨平台,生态好,使用广泛;
- bazel的BUILD使用对象来描述编译规则,依赖清晰,新生代社区活跃,适合大型工程;
最后,上述三个编译模板我都放到github了,按需自取:https://github.com/jegbrother/CPPCompileTemplatehttps://github.com/jegbrother/CPPCompileTemplate
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)