C/C++项目的编译, Makefile和CMake
Posted tms不熬夜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++项目的编译, Makefile和CMake相关的知识,希望对你有一定的参考价值。
LuaJIT
的 src
目录下, 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 src
HOSTCC host/minilua.o
HOSTLINK host/minilua
DYNASM host/buildvm_arch.h
HOSTCC host/buildvm.o
HOSTCC host/buildvm_asm.o
...
CC lib_jit.o
CC lib_ffi.o
CC lib_init.o
AR libluajit.a
CC luajit.o
BUILDVM jit/vmdef.lua
DYNLINK libluajit.so
ld: warning: -seg1addr not 16384 byte aligned, rounding up
LINK luajit
OK 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)
636
637 host/buildvm_arch.h: $(DASM_DASC) $(DASM_DEP) $(DASM_DIR)/*.lua lj_arch.h lua.h luaconf.h
638 $(E) "DYNASM $@"
639 $(Q)$(DASM) $(DASM_FLAGS) -o $@ $(DASM_DASC)
640
641 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
命令, 会得到:
make
echo "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 = gcc
CFLAGS = -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 = gcc
CFLAGS = -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= Windows
160 else
161 HOST_SYS:= $(shell uname -s)
162 ifneq (,$(findstring MINGW,$(HOST_SYS)))
163 HOST_SYS= Windows
164 HOST_MSYS= mingw
165 endif
166 ifneq (,$(findstring MSYS,$(HOST_SYS)))
167 HOST_SYS= Windows
168 HOST_MSYS= mingw
169 endif
170 ifneq (,$(findstring CYGWIN,$(HOST_SYS)))
171 HOST_SYS= Windows
172 HOST_MSYS= cygwin
173 endif
174 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_app
SRC_DIR = ./src
BUILD_DIR = ./build
LD_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)
# Executable
my_app: $(SRCS)
$(CC) -c $(SRCS) $(INC_FLAGS); mv *.o $(BUILD_DIR)
$(CC) -o $(BUILD_DIR)/$@ $(addprefix $(BUILD_DIR)/,*.o) $(LIBS)
# Dynamic library
my_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)
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的主要内容,如果未能解决你的问题,请参考以下文章