Makefile好难写
曾经也总结了一篇关于Makefile的文章《make和makefile的简单学习》。但是,总结完以后,发现写Makefile真的是一件非常痛苦的事情,的确非常痛苦。而更痛苦的是当需要将代码移植到别的系统上时,这就够你喝一壶的。再说了,作为程序员的我们,是不是更应该投入更多的精力到业务逻辑的编写与处理中呢,而并不是和Makefile纠结呢。
他们是怎么做到的?
不知道你是否阅读过一些Linux平台上开源的C或者C++项目,当你编译这些项目的时候,只需./configure
、 make
和make install
就可以把程序编译完成并安装到系统中。你是否想过,这些开源的项目的编译和安装怎么如此的干净利落。
我们觉的写Makefile很麻烦,我们羡慕那些开源项目简单的编译安装步骤。那么如何让写Makefile变得简单一些呢?这个时候大师们就编写了一些能够自动根据系统生成Makefile文件的工具。这篇文章,我就对这些工具进行简单的总结,并结合一个简单的例子进行实践。
工具简介
大师们写的工具主要有哪些呢?如下所示:
- GNU Automake
- GNU Autoconf
- GNU m4
- GNU Libtool
如果你想使用这些工具,就先看看你的系统上有没有正确安装这些工具(我使用的是Ubuntu)。
如果没有安装,则执行以下语句安装就OK了。
autoconf是一个用于生成可以自动地配置软件源码包,用以适应多种UNIX类系统的shell脚本工具,其中autoconf需要用到m4,便于生成脚本。automake是一个从Makefile.am文件自动生成Makefile.in的工具。为了生成Makefile.in,automake还需用到perl,由于automake创建的发布完全遵循GNU标准,所以在创建中不需要perl。libtool是一款方便生成各种程序库的工具。
对于工具的简单介绍就到此结束,接下来讲讲如何使用这些工具来生成一个Makefile文件。
自动生成makefile的步骤
使用上述的工具生成Makefile文件的步骤基本是死的,只需要在每步按照我们的需求进行适当的配置即可生成一个“漂亮”的Makefile文件。具体的步骤如下:
- 运行autoscan命令
扫描源代码以搜寻普通的可移植性问题,比如检查编译器、库、头文件等,生成文件configure.scan,它是configure.ac的一个雏形。 - 将configure.scan文件重命名为configure.ac,并按照需要修改configure.ac文件
configure.ac文件的内容是一些宏,confiugre.ac调用一系列autoconf宏来测试程序需要的或用到的特性是否存在,以及这些特性的功能。这些宏经过autoconf处理后会变成检查系统特性、环境变量、软件必须的参数的shell脚本。configure.ac文件中的宏的顺序并没有规定,但是你必须在文件的最前面和最后面分别加上AC_INIT宏和AC_OUTPUT宏。在configure.ac中的一些常用宏定义。
宏名称 | 说明 |
AC_PREREQ | 声明autoconf要求的版本号 |
AC_INIT | 定义软件名称、版本号、联系方式 |
AM_INIT_AUTOMAKE | 必须要的,参数为软件名称和版本号 |
AC_CONFIG_SCRDIR | 用来侦测所指定的源码文件是否存在, 来确定源码目录的有效性 |
AC_CONFIG_HEADER | 用于生成config.h文件,以便autoheader命令使用 |
AC_PROG_CC | 指定编译器,默认GCC |
AC_CONFIG_FILES | 生成相应的Makefile文件,不同文件夹下的Makefile通过空格分隔。例如:AC_CONFIG_FILES([Makefile, src/Makefile]) |
AC_OUTPUT | 用来设定configure所要产生的文件,如果是Makefile,configure会把它检查出来的结果带入Makefile.in文件产生合适的Makefile |
- 执行aclocal命令
aclocal是一个perl 脚本程序。aclocal根据configure.ac文件的内容,自动生成aclocal.m4文件。 - 执行autoheader命令
该命令生成config.h.in文件。该命令通常会从acconfig.h文件中复制用户附加的符号定义。 - 执行autoconf命令
有了configure.ac和aclocal.m4 两个文件以后,我们就可以使用autoconf来产生configure文件了。configure脚本能独立于autoconf运行,且在运行的过程中,不需要用户的干预。 - 在Project目录下新建Makefile.am文件
- 运行automake命令
automake会根据Makefile.am文件产生一些文件,其中最重要的是Makefile.in文件。 - 执行configure生成Makefile。
这些命令之间的关系如下图所示:
以上就是生成一个完整makefile的主要步骤。当然了,在实际项目中,会根据需要进行微调。下面我就拿最经典的Hello World
程序进行一个简单的演示。
从Hello World开始
测试代码:HelloWorld.cpp
源码如下:
- 运行autoscan命令,生成的文件列表如下:
- 重命名configure.scan文件为configure.ac,修改configure.ac文件为如下样子:
- 执行aclocal命令,生成的文件列表如下:
- 执行autoheader命令,生成config.h.in文件
- 执行autoconf命令,生成的文件列表如下:
可以看到,生成了可执行的configure
文件。
- 在Project目录下新建Makefile.am文件,Makefile.am文件的内容如下:
关于如何编写Makefile.am文件,我在下一节总结。
- 运行automake命令,就会得到Makefile.in文件
执行automake命令时,提示有些文件不存在,我们直接
touch
- 即可。
- 执行configure生成Makefile
现在得到我们需要的Makefile文件了,接下来,你应该怎么做了。
如何编写Makefile.am
上面对于一个简单的HelloWorld程序使用autoconf和automake工具成功的生成了Makefile文件,对于编写Makefile.am文件一直没有详细说明,这里就对如何编写Makefile.am文件进行详细的说明。
Makefile.am是一种比Makefile更高层次的规则。只需指定要生成什么目标,它由什么源文件生成,要安装到什么目录等即可。automake会根据我们写的Makefile.am来自动生成Makefile.in。Makefile.am中定义的宏和目标会指导automake生成指定的代码。常见的文件编译类型有下面几种:
- PROGRAMS;表示可执行文件
- LIBRARIES;表示库文件
- LTLIBRARIES;这也是表示库文件,前面的LT表示libtool
- HEADERS;头文件
- DATA;数据文件,不能执行。
下面就对编译成可执行文件、动态库文件和静态库文件常用的写法进行简单介绍。
- 编译可执行文件
比如这样的一个Makefile.am文件:
每个字段具体的含义如下:
名称 | 含义 |
bin_PROGRAMS | 表示指定要生成的可执行应用程序文件,这表示可执行文件在安装时需要被安装到系统中;如果只是想编译,不想被安装到系统中,可以用noinst_PROGRAMS来代替 |
client_SOURCES | 表示生成可执行应用程序所用的源文件,这里注意,client_是由前面的bin_PROGRAMS指定的,如果前面是生成example,那么这里就是example_SOURCES,其它的类似标识也是一样 |
client_CPPFLAGS | 这和Makefile文件中一样,表示C语言预处理参数,这里指定了DCONFIG_DIR,以后在程序中,就可以直接使用CONFIG_DIR。不要把这个和另一个CFLAGS混淆,后者表示编译器参数 |
client_LDFLAGS | 连接的时候所需库文件的标识,这个也就是对应一些如-l,-shared等选项 |
noinst_HEADERS | 这个表示该头文件只是参加可执行文件的编译,而不用安装到安装目录下。如果需要安装到系统中,可以用include_HEADERS来代替 |
INCLUDES | 链接时所需要的头文件 |
client_LDADD | 链接时所需要的库文件,这里表示需要两个库文件的支持 |
SUBDIRS | 表示在处理本目录之前需要递归处理哪些子目录 |
- 编译动态库文件
想要编译XXX.so文件,需要用_PROGRAMS类型,这里一个关于安装路径要注意的问题是,我们一般希望将动态库安装到lib目录下,只需要写成lib_PROGRAMS就可以了,因为前面的lib表示安装路径(为什么?稍后讲),但是automake不允许这么直接定义,可以采用下面的办法,也是将动态库安装到lib目录下。
- 编译静态库文件
对于下面的一个Makefile.am文件:
对上述Makefile.am文件的解释如下:
名称 | 含义 |
noinst_LTLIBRARIES | 这里要注意用的是LTLIBRARIES,另外还有LIBRARIES,两个都表示库文件。前者表示libtool库,用法上基本是一样的。如果需要安装到系统中的话,用lib_LTLIBRARIES。一般推荐使用libtool库编译目标,因为automake包含libtool,这对于跨平台可移植的库来说,肯定是一个福音。 |
libutil_a_LIBADD | 静态库编译连接时需要其它的库的话,采用XXXX_LIBADD选项 |
- 头文件我们一般需要导入一些*.h的头文件,如果你在Makefile.am中没有标识需要导入的头文件,可能在make dist打包的时候出现问题,头文件可能不会被打进包里面。
make install,头文件默认会被安装到linux系统/usr/local/include。
- 数据文件
在上面说到一个问题,在编译动态库文件时:
写成lib_PROGRAMS就可以了,因为前面的lib表示安装路径,为什么?
这里涉及到编写Makefile.am文件时,安装路径的问题。
安装路径
在默认的情况下,执行make install
命令,则会将文件安装到$(prefix) = /usr/local
路径下。我们可以通过./configure --prefix=xxx
的方式来进行修改。基于此,系统还定义了以下一些路径变量:
名称 | 值 |
bindir | $(prefix)/bin |
libdir | $(prefix)/lib |
datadir | $(prefix)/share |
sysconfdir | $(prefix)/etc |
includedir | $(prefix)/include |
那么现在你就应该明白以下知识点了:
bin_PROGRAMS
- 表示将生成的可执行文件安装到
$(bindir)
- 目录下
lib_LTLIBRARIES
- 表示将静态库安装到
$(libdir)
- 目录下
- 上面说的
projectlib_PROGRAMS
- 表示安装到
$(projectlibdir)
- 所表示的目录下
如果我们在Makefile.am文件中定义了一个新的路径:
此时data1和data2就会作为数据文件安装到$(prefix)/sysdate
路径下。现在关于安装路径的文件就应该明白了吧。
打包
一切搞定以后,可以正常的生成Makefile文件,编译生成的程序或库也没有任何问题了,此时我们可能需要对源文件进行打包。使用make dist
命令就可以完成自动打包任务,自动打包包含的内容如下:
- 所有源文件
- 所有的Makefile.am文件
- configure读取的文件
- Makefile.am中包含的文件
- EXTRA_DIST指定的文件
- 采用dist及nodist指定的文件,如可以将某一源文件指定为不打包:
使用make dist
命令之后,就会在当前目录下生成一个在AC_INIT
中定义的软件名称和版本号的tar.gz
压缩包。
总结
总结的好长的一篇文章,文章虽长,但讲的不深,入门足够。更多的相关知识只有通过更多在实际项目中锤炼,阅读更多的相关文档学习了。
为了更好的学习这方面的知识,大伙也可以阅读一些Linux平台的开源代码,看看这些开源代码的Makefile是如何生成的。最后,祝各位好运。