C/C++项目的编译, Makefile和CMake

Posted tms不熬夜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++项目的编译, Makefile和CMake相关的知识,希望对你有一定的参考价值。

LuaJITsrc 目录下, Makefile有700多行, 其中大部分是在判断编译环境和声明依赖关系.


在 Mac 上编译LuaJIT , 需要把 MacOS 的版本声明在环境变量里, 不然编译会报错. 在关于里查看系统版本:


设置环境变量:

export MACOSX_DEPLOYMENT_TARGET=11.2

然后就可以编译LuaJIT了:

make==== Building LuaJIT 2.1.0-beta3 ====/Applications/Xcode.app/Contents/Developer/usr/bin/make -C srcHOSTCC host/minilua.oHOSTLINK host/miniluaDYNASM host/buildvm_arch.hHOSTCC host/buildvm.oHOSTCC host/buildvm_asm.o...
CC lib_jit.oCC lib_ffi.oCC lib_init.oAR libluajit.aCC luajit.oBUILDVM jit/vmdef.luaDYNLINK libluajit.sold: warning: -seg1addr not 16384 byte aligned, rounding upLINK luajitOK Successfully built LuaJIT==== Successfully built LuaJIT 2.1.0-beta3 ====

之后我们会一步一步的弄清楚 LuaJIT 的代码. 这里先看一看 Makefile:

633 $(MINILUA_T): $(MINILUA_O)634 $(E) "HOSTLINK $@"635 $(Q)$(HOST_CC) $(HOST_ALDFLAGS) -o $@ $(MINILUA_O) $(MINILUA_LIBS) $(HOST_ALIBS)636637 host/buildvm_arch.h: $(DASM_DASC) $(DASM_DEP) $(DASM_DIR)/*.lua lj_arch.h lua.h luaconf.h638 $(E) "DYNASM $@"639 $(Q)$(DASM) $(DASM_FLAGS) -o $@ $(DASM_DASC)640641 host/buildvm.o: $(DASM_DIR)/dasm_*.h

编译的过程基本是从 Makefile 的 633 行开始, 顺序的编译出对应的文件. 而 Makefile 里很多的符号看起来比较复杂, 不弄清楚的话, 对我们了解 LuaJIT 的编译过程还是有影响的. 下面就介绍一下 Makefile.


#1. Makefile 是什么?

C/C++ 的项目, 简单的编译可以直接敲命令行完成, 但随着项目复杂度的提升, 直接敲命令行就不大现实了, 比如要编译多个不同的版本, 根据文件更新时间做部分重编译, 根据环境变量配置 include 目录, 面向不同指令集架构做编译等. 这时候就轮到 Makefile 出场了, 通过它可以灵活的配置编译的参数. Makefile + make, 基本等于 Java 的 Maven.


#2. Makefile的基本语法

最简单的 Makefile:

hello: echo "Hello Makefile"

这时候执行 make 命令, 会得到:

makeecho "Hello Makefile"Hello Makefile

Makefile 的基本语法是:

输出文件名: 依赖的文件1 依赖的文件2 ... 需要执行的脚本 需要执行的脚本  ...


比如, 从 hello.c 编译出可执行文件 hello_app:


hello_app: hello.c    gcc -o hello_app hello.c

有一点需要注意的是, Makefile 的缩进必须是 tab 字符, 使用空格缩进会出现解析错误.


#3. 稍微灵活一点?

设置和使用参数:


CC = gccCFLAGS = -I.
hello_app: hello.c    $(CC) -o hello_app hello.c $(CFLAGS)


设置参数有两种方式, =:= . 通过 = 赋值的变量, 会在执行时去取值, 而通过 := 赋值, 会在定义时取值:

a = $(x)b := $(x)
x = "这里 a 会是这段字符串, 但 b 会是一个空字符"


扩展名依赖, 比如.o文件依赖于.c文件和.h文件, 当.c 文件或.h 文件更新时重新编译:

CC = gccCFLAGS = -I.
%.o: %.c %.h $(CC) -c -o $@ $< $(CFLAGS)
hello_app: hello.c    $(CC) -o hello_app hello.c $(CFLAGS)


其中 $@ 表示文件名, 这里是hello.o . $< 表示依赖里的第一个, 这里是hello.c . 另外$^ 表示所有的依赖, 在这里是hello.c hello.h.


Makefile 提供了条件判断:

158 ifeq (Windows,$(findstring Windows,$(OS))$(MSYSTEM)$(TERM))159 HOST_SYS= Windows160 else161 HOST_SYS:= $(shell uname -s)162 ifneq (,$(findstring MINGW,$(HOST_SYS)))163 HOST_SYS= Windows164 HOST_MSYS= mingw165 endif166 ifneq (,$(findstring MSYS,$(HOST_SYS)))167 HOST_SYS= Windows168 HOST_MSYS= mingw169 endif170 ifneq (,$(findstring CYGWIN,$(HOST_SYS)))171 HOST_SYS= Windows172 HOST_MSYS= cygwin173 endif174 endif

也提供了丰富的内置函数如 findstring, subst , patsubst , foreach , if , call 等:

bar := $(subst not, totally, "I am not superman")
all: # 会输出 I am totally superman    echo $(bar)

Makefile 也可以有模块化的写法, 可以在一个Makefile里引入另一个Makefile :

include filenames...

#4. 中小型C++项目的通用模板

CC = clang++
EXECUTABLE = my_appSRC_DIR = ./srcBUILD_DIR = ./buildLD_LIB_PATH = ./lib
SRCS = $(shell find $(SRC_DIR) -name *.cpp -or -name *.c)INC_DIRS = $(shell find $(SRC_DIR) ./include -type d)LIBS = $(shell find $(LD_LIB_PATH) -name *.so)
INC_FLAGS = $(addprefix -I,$(INC_DIRS))CPPFLAGS = $(INC_FLAGS)
# Executablemy_app: $(SRCS) $(CC) -c $(SRCS) $(INC_FLAGS); mv *.o $(BUILD_DIR) $(CC) -o $(BUILD_DIR)/$@ $(addprefix $(BUILD_DIR)/,*.o) $(LIBS)
# Dynamic librarymy_lib: $(SRC_DIR)/my_lib.cpp $(SRC_DIR)/my_lib.h $(CC) -c -o $(BUILD_DIR)/$@.o $(SRC_DIR)/$@.cpp $(INC_FLAGS) $(CC) -shared -fPIC -DBUILD_SHARED_LIBS=OFF -o $(LD_LIB_PATH)/$@.so $(BUILD_DIR)/$@.o $(LIBS)
.PHONY: clean exec my_all my_lib
clean: rm -r $(BUILD_DIR)/*
exec: make && $(BUILD_DIR)/$(EXECUTABLE)

#5. 跨平台编译

上面的模板是 Mac 上的编译文件, 所以用的是 clang++, 在编译.so 文件的时候还带了-DBUILD_SHARED_LIBS=OFF 这个选项, 不然就会编成 Mac 上的dylib 文件.


现在大的项目, 因为在本地的编译时间过长, 很多都迁移到了云端, 利用云端一致的环境和更高的计算能力来加速编译. 对多人协作的项目来说, 项目中有的人用 Windows, 有的人用 Linux, 有的人用 Mac 的情况也是常态, 所以编译也需要跨平台, 最好所有的人, 代码下下来直接敲 make 命令就能编译成功.


这样无疑让编写Makefile变得更加繁琐, 因为需要考虑到多个平台, 写完还需要在这多个平台做测试. 于是就出现了跨平台编译的工具, 如 SCons, CMake, Bazel, 和 Ninja. CMake 是受众最广的一个(Qt 升级到 6.0 的时候也开始使用 CMake 做编译).


CMake 的好处, 光从配置文件的区别就能看出来, 比如一个 CMakeLists.txt:

cmake_minimum_required(VERSION 3.1...3.18 FATAL_ERROR)
project(my_app VERSION 1.0)set(CMAKE_CXX_STANDARD 14)add_executable(my_app ./src/main.cpp)
add_library(my_lib ./lib/my_lib/my_lib.cxx)add_subdirectory(./lib/my_lib)target_link_libraries(my_app PUBLIC -L./lib/my_lib ./lib/my_another_lib)target_include_directories(my_app PUBLIC "./include")

CMake 的配置项文件不但字少, 读起来也很容易, 每个函数的名称基本就代表了配置含义. CMake 同样也可以执行复杂的判断, 循环, 字符串处理等.


C++ 的编译配置一直都比较复杂, 但 CMake 提供了一种平台无关的抽象, 已经为开发者减轻了不少工作. 现在的开发者, 只需要一个不算冗长的 CMakeLists.txt 就可以配置好一个 C++ 项目了.

以上是关于C/C++项目的编译, Makefile和CMake的主要内容,如果未能解决你的问题,请参考以下文章

Makefile项目管理-----在Linux下编译c/c++程序

如何写 makefile

升级构建工具,从Makefile到CMake

升级构建工具,从Makefile到CMake

入门C/C++自动构建利器之Makefile

入门C/C++自动构建利器之Makefile