0%

c++项目构建工具简单介绍

不使用任何构建工具

对于C/C++代码来说,不使用任何构建工具来辅助编译C/C++代码的话,就需要自己手动指定使用的编译器(clang、gcc/g++或者其他编译器),一般来说需要在编译命令下指定这些:

  • 指定需要编译的源文件
  • 指定头文件的搜索目录
  • 指定库文件的搜索目录
  • 需要链接的动态库和静态库
  • 指定需要额外定义的宏或者取消定义的宏来控制编译
  • 指定编译优化选项
  • 指定语言标准
  • 等等

以gcc/g++为例,使用-D指定额外定义的宏,-U取消宏定义:

1
2
3
-DUseXX //#define UseXX
-DMaxSize=500 //#define MaxSize 500
-UUseXX //#undef UseXX

-I(i的大写) 添加头文件搜索目录

1
-I/usr/local/include/xxx -I/usr/local/Cellor/yyy

-L 添加库文件搜索目录

1
-L/usr/local/lib -L/usr/local/Cellor/lib

-l(L的小写)添加链接库

1
-la -lb -lz

-g生成调试信息

1
2
3
4
-g #以操作系统的本地格式产生调试信息
-g1 #输出的调试信息
-g2 #输出默认量的调试信息
-g3 #输出更多的调试信息

警告选项

1
2
3
-w #关闭显示所有警告信息
-Wall #开启大部分警告提示,默认
-Werror #将警告视为错误,出现警告即放弃编译

-O优化选项

1
2
3
4
-O0 #不优化
-O1 #
-O2
-O3

语言选项

1
2
3
-ansi #支持符合ANSI标准的C程序
-std=c99 #C99标准
-std=c++11 # c++11标准

直接使用gcc/g++命令在使用代码文件不多,库直接的依赖关系不复杂的情况下是可以接受的。但是在一些大型项目中,源文件上千上万,各个文件和库直接的依赖关系复杂,如果还手动输入编译命令的话,复杂度就会变得很大。

make工具

make工具是一个从源码自动构建所需“目标”的构建工具,他依赖通过读取一个叫做Makefile的脚本文件来实现自动化构建。

Makefile本身其实一种管理代码源文件之间依赖关系的脚本,在这个脚本中描述了项目工程中的各个“目标”或者源文件之间的依赖关系以及构建各个目标所需要的命令。

斯图亚特·费尔德曼在1977年在贝尔实验室里制作了最初的make程序,最初的make程序被多次重/改写,形成了多种版本的make工具,比较出名的有GNU make, BSD make,microsoft nmake等等。这些make工具都有不同的规范和标准,使用的Makefile的语法标准也不同。

下面以GNU Make使用的Makefile的格式对Makefile进行简介。

GNU Makefile格式

编写Makefile的规则一般如下:

1
2
3
4
5
6
7
8
9
10
target: prerequisites
[tab]COMMAND
[tab]COMMAND
...
...
...

target2: prerequisites
...
...

Makefile中target就是make命令中所需指定的构建目标。make会把在Makefile中定义的第一target作为默认target,即如果不给make命令传递target参数,则默认为makefile中的第一个定义的target。

prerequisites说明了这个target依赖哪些其他的target或者文件,这个字段可以为空,说明这个target没有依赖。如果target有依赖的情况下,make会记录target是否有更新,如果依赖没有更新,make就不会重复构建target。

command就是这个生成这个target所需要的命令,这个命令可以是任意的命令

注意,可能有一个误区就是prerequisites字段只是用来让make进行依赖解析和判断target是否需要重建的,依赖和command本身没有任何关系。如果以为某个target依赖了某些源文件,那么make在解析的时候自动会将这些源文件传递给命令,这是不对的。例如target main依赖了main.cpp文件,那么这么写是没有用的:

1
2
main: main.cpp
g++ -o main

应该这么写:

1
2
main: main.cpp
g++ -o main main.cpp

下面举一个例子来使用makefile构建一个可执行文件,在a.h定义了类A,在a.cpp中实现了类A的方法,在main.cpp中引入了a.h使用了类A的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//a.h
class A {
public:
void say();
};

//c.cpp
#include "a.h"
#include <iostream>
void A::say() {
std::cout << "hello world" << std::endl;
}

//main.cpp
#include "a.h"
int main() {
A a;
a.say();
return 0;
}

对应的Makefile的一种形式

1
2
3
4
5
6
7
8
9
10
11
CXX = g++
main: a.cpp.o main.cpp.o
$(CXX) -o test main.o a.o

main.o: a.h main.cpp
echo "main"
$(CXX) -c main.cpp -o main.o

a.o: a.h a.cpp
echo "a"
$(CXX) -c a.cpp -o a.o

还有很多其他写法:

1
2
3
CXX = g++
main: a.h a.cpp main.cpp
$(CXX) -o test main.cpp a.cpp

但是这种写法不好的地方在于,如果a.h,a.cpp,main.cpp中有任意一个文件有变化,这几个文件都要重新再编译一次。

执行make命令就会构建出目标main,生成test可执行文件。

1
make #等价使用make main

也可以不构建目标main,选择构建其他目标,例如构建main.cpp.o目标:

1
make main.cpp.o #执行g++ -c main.cpp,生成main.o文件

make对target依赖的解析是一个类似递归的过程,例如上例中目标main依赖了目录a.cpp.o和main.cpp.o那么make会先去执行a.cpp.o下的命令和main.cpp.o下的命令,然后再来执行目标main定义的命令。

在make file中有几个特殊的target,一个是clean,另外一个是install,不是说这些命令必须执行某种命令,而是大家默认了使用cmake执行这些target应该做的事,clean就应该是去清除之前构建目标生成出来的文件,而install应该将构建目标生成出来的文件安装到指定目录下。你也可以在Makefile中将clean定义为执行安装操作的命令,然后把install定义为执行clean操作的命令,但是这不合理。

之前提到Makefile本质上就是一个脚本文件,既然是脚本文件,那就可以在Makefile中定义变量,使用变量,使用条件控制语句,定义函数,使用函数,include其他Makefile文件等。这些使用可以去网上自己去搜索。

Makefile本身只关心各个文件或目标之间的依赖关系,省去了开发者在修改某些代码文件后判断需要重新编译生成目标文件的时间。但是在Makefile中最后还是需要自己去调用编译器命令,在命令中指定include的目录,链接库的路径,需要链接的库。另外在不同的操作系统下还需要指定不同的编译器,指定不同的include头文件目录等等。

最后说明一下,由于Makefile本身不关心编译过程的,因为最后的编译还是通过在Makefile中编写调用编译命令,所以使用make+Makefile理论上可以用来编译任何代码。

GNU Autotools

之前说到在不同的代码运行平台下,可能需要在Makefile中指定不同的编译器的名称,指定不同的头文件路径,链接不同的链接库等等,如果用make工具,那么就需要对不同的运行平台,就需要编写不同的Makefile,这不仅会增加工作量,而且要求程序员对各个平台也要有一定的熟悉程度。GNU Autotools的目的就是为了在不同类Unix平台下,针对GNU make工具快速生成对应的Makefile文件。

GUN Autotools是一个GNU工具集,它包括autoscan、autoheader,autoconf,automake,aclocal和libtool等等。最主要使用的两个工具就是autoconf和automake。使用Autotools最终也会生成Makefile文件,然后调用make命令来构建出项目。使用Autotools构建项目的三部曲一般是./configure,make,make install。

运行configure脚本用于系统检测,一般主要检测当前平台的编译器、库文件、头文件等等,这些检查的结果将用于将config.h.in和Makefile.in文件转化为最终的config.h和Makefile文件。configure脚本一般有几千行,非常复杂,如果要手写的话很困难,可以使用autoconf工具来自动生成这个configure脚本。autoconf工具通过读取一个configure.ac的模版文件,来生成configure脚本。configure.ac文件是一个由m4宏编写的文件,autoconf在处理configure.ac文件时就会将这些宏展开为shell脚本代码。

在开始的时候,可以使用autoscan工具来生成一个configure.ac的模版,autoscan生成的文件的名字为configure.scan,直接修改configure.scan名字为configure.ac即可。

再以写Makefile时的示例项目为例,通过对autoscan生成的模版再进行修改的情况下,编写的configure.ac文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ([2.69]) #autoconf版本最低要求2.69
AC_INIT([test], [1.0], [www.xxx-bug-report.com]) #设置项目名称,版本号,bug上报网址
AM_INIT_AUTOMAKE # 项目需要使用automake,初始化automake
AC_CONFIG_FILES([Makefile]) #告诉configure 根据系统信息和Makefile.in生成Makefile文件

# Checks for programs.
AC_PROG_CXX #告诉configure 检查c++编译器,由于没有指定这个宏的参数,默认参数为g++
AC_PROG_CC #告诉configure 检查c编译器,由于没有指定这个宏的参数,默认参数为gcc

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_OUTPUT

configure脚本会根据Makefile.in来生成Makefile,但是Makefile.in的脚本也是又长又复杂的,但是和configure的生成一样,Makefile.in可以由一个模版文件通过automake来生成,这个模版文件就是Makefile.am。

在这个例子下,对应的Makefile.am:

1
2
3
4
5
6
7
8
AUTOMAKE_OPTIONS = foreign #因为这里的例子不是标准的 GNU 项目的结构,所以结构声明为 foreign
bin_PROGRAMS = test #告诉automake 需要Makefile构建的项目是一个可执行文件,其名称为test
test_SOURCES = main.cpp a.cpp #告诉automake 项目使用的源文件

#test_CPPFLAGS = #设置c++编译的编译选项,这里不需要额外的编译选项
#test_LDFLAGS = #设置链接选项,这了不需要额外的链接选项,
#test_LDADD = #设置要链接的库文件,这里也没有
#INCLUDES = #设置头文件搜索目录,这里也没有额外的头文件

接下来,依次运行下面几个命令即可完成对项目的编译

1
2
3
4
5
aclocal #使用aclocal初始化m4环境
autoconf #将configure.ac生成configure脚本
automake --add-missing #将Makefile.am生成Makefile.in
./configure #检查系统环境,将Makefile.in和config.h.in(这里没有使用到)生成Makefile和config.h
make

在发布软件的时候,不需要将configure.ac和Makefile.am文件发布出去,只需要将Makefile.in,config.in.h和configure脚本发布出去即可。

GNU Autotools工具对类Unix系统的用户来说比较友好,对于使用windows系统的用户来说就很不友好了,需要进行很多的配置才行。

CMake

之前提到不同的平台下,不同的IDE间使用的make工具的规范和标准是不同的,Makefile的标准也就不同,CMake的出现解决了不同make工具之间规范和标准不同的问题。

CMake让构建人员通过一个与平台无关的CMakeList.txt文件来说明这个项目的构建流程,然后再根据目标平台生成对应的Makefile文件。

CMakeList.txt是使用CMake自己的一套语法来编写的。还是以之前的例子来说明一个CMaList.txt有哪些内容:

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.13) #cmake最低版本3.13
project(Test) #设置CMake项目名称

set(CMAKE_CXX_STANDARD 11) #设置C++标准为C++11

set(TEST_SRC main.cpp a.cpp)

add_executable(test ${TEST_SRC}) #添加说明要构建一个可执行文件,文件名为test,源码文件为main.cpp,a.cpp

之后执行命令:

1
2
cmake .
make

就可以完成对项目的编译构建。

CMake会自动检查当前平台使用的编译器信息,当然也可以在CMake中要使用的编译器

总结

代码构建工具的目的

1、代码构建工具的首要目的就是帮助程序员能够从代码构建出最后的产物

2、帮助管理代码中的依赖问题

3、加快编译构建速度,能够并行编译,并且只对需要重新编译的部分进行编译。

为什么c/c++的项目这么难管理

以下原因只是我的主观看法:

1、c/c++发展的太早,早到发展初期根本没有想到依赖管理的问题,早到当时根本没有考虑各个平台下统一标准,统一规范的问题。

2、

3、与系统强相关,在不同系统中,使用的c/c++的头文件,库文件都是不同的,另外,在一个平台下编译出来的二进制文件有可能无法在另外一个平台下使用,需要从源码重新编译。

TODO: 将PPT中的一些内容补充到这篇博客中