
引子一个令人崩溃的下午1991年的某个下午一个叫David MacKenzie的程序员坐在电脑前第37次修改他的Makefile。他写了一个C程序在自己的Linux机器上编译运行得好好的。然后他把代码发给同事——同事用的是SunOS。编译失败。因为SunOS的C库里没有strerror()函数。他加了一个#ifdef绕过去了。把代码发给另一个同事——这位用的是HP-UX。又编译失败。因为HP-UX的头文件string.h和strings.h的关系跟Linux不一样。他又加了一个#ifdef。然后是AIX、IRIX、Ultrix、BSD、Minix……每个系统都有自己的怪癖每个系统都需要不同的#ifdef。他的代码开始变成这样#ifdefHAVE_STRERRORmsgstrerror(errno);#elsemsgUnknown error;#endif#ifdefHAVE_STRING_H#includestring.h#else#ifdefHAVE_STRINGS_H#includestrings.h#endif#endif#ifdefHAVE_UNISTD_H#includeunistd.h#endif#ifdefHAVE_SYS_WAIT_H#includesys/wait.h#endif代码里一半是逻辑一半是#ifdef。更要命的是——谁来定义这些HAVE_XXX宏你得在编译之前先检测目标系统上有没有strerror有没有string.h有没有unistd.h……然后根据检测结果生成一个config.h文件把对应的宏定义写进去。David MacKenzie受够了。他决定写一个工具自动完成这些检测工作。这个工具就是Autoconf。第一章Autoconf到底是什么一句话定义Autoconf是一个把configure.ac检测需求清单变成configure检测执行脚本的工具。用一个比喻来说——你要装修一套房子。装修之前你需要搞清楚这套房子的情况- 墙体是砖墙还是混凝土决定能不能打孔 - 电线是铜芯还是铝芯决定用什么接头 - 水管是PPR还是PVC决定用什么胶水 - 有没有预埋燃气管决定能不能装燃气灶 - 层高多少决定能不能做吊顶 - 承重墙在哪决定哪些墙不能拆你可以自己一项一项去检查。但如果你要装修一百套不同的房子呢每套房子的情况都不一样你不可能每次都从头检查。于是你写了一份检测清单□ 检查墙体材质 □ 检查电线类型 □ 检查水管材质 □ 检查有无燃气管 □ 检查层高 □ 检查承重墙位置然后你雇了一个检测员把清单交给他。他拿着清单去每套房子里逐项检查最后给你一份检测报告告诉你这套房子的具体情况以及应该用什么装修方案。在这个比喻里检测清单 configure.ac你写的 检测员 autoconf工具本身 检测报告生成器 configureautoconf生成的脚本 检测报告 config.h Makefileconfigure生成的结果 被检测的房子 目标操作系统工作流程全景你开发者写的文件 autoconf生成的文件 configure运行后生成的文件 configure.ac ──→ [autoconf] ──→ configure ──→ [运行] ──→ config.h Makefile config.status config.log让我们逐步拆解每一个环节。第二章configure.ac——检测需求清单这个文件长什么样configure.ac是你写给Autoconf的需求清单。它用一种特殊的宏语言M4宏编写。以Mono项目为例它的configure.ac大致是这样的极度简化版# # 第一部分项目基本信息 # AC_INIT([mono], [6.12.0], [mono-bugslists.dot.net]) # 翻译这个项目叫mono版本6.12.0Bug报告发到这个邮箱 AC_CONFIG_SRCDIR([mono/mini/mini.c]) # 翻译用这个文件来验证源码目录是否正确 # 防止你在错误的目录下运行configure AM_INIT_AUTOMAKE([foreign]) # 翻译初始化Automake用foreign模式 # 不要求GNU项目的标准文件如NEWS、AUTHORS # # 第二部分检测编译工具 # AC_PROG_CC # 翻译找一个能用的C编译器 # 会依次尝试 gcc, cc, cl 等 # 找到后把路径存到变量 $CC 里 AC_PROG_CXX # 翻译找一个能用的C编译器 AC_PROG_INSTALL # 翻译找到 install 命令用于安装文件 AC_PROG_LN_S # 翻译检查系统是否支持符号链接 # # 第三部分检测头文件 # AC_CHECK_HEADERS([sys/mman.h]) # 翻译检查系统有没有 sys/mman.h 这个头文件 # 如果有在config.h里定义 HAVE_SYS_MMAN_H AC_CHECK_HEADERS([sys/socket.h netinet/in.h]) # 翻译检查网络相关的头文件 AC_CHECK_HEADERS([pthread.h]) # 翻译检查POSIX线程的头文件 # # 第四部分检测库和函数 # AC_CHECK_LIB([pthread], [pthread_create]) # 翻译检查系统有没有pthread库 # 具体方法是尝试链接一个调用pthread_create的程序 # 如果成功说明pthread库存在 AC_CHECK_FUNCS([strerror mmap getpagesize sysconf]) # 翻译逐个检查这些函数是否存在 # 存在的话在config.h里定义对应的 HAVE_STRERROR 等宏 AC_CHECK_FUNCS([dlopen], [], [ AC_CHECK_LIB([dl], [dlopen]) ]) # 翻译检查dlopen函数 # 如果直接找不到再去libdl库里找 # # 第五部分检测第三方库 # PKG_CHECK_MODULES(GLIB, glib-2.0 2.28) # 翻译用pkg-config检查GLib库 # 要求版本 2.28 # 如果找到设置 GLIB_CFLAGS 和 GLIB_LIBS 变量 # # 第六部分检测系统特性 # AC_C_BIGENDIAN # 翻译检查CPU是大端还是小端 # x86是小端某些ARM和PowerPC是大端 AC_CHECK_SIZEOF([void *]) # 翻译检查指针的大小 # 32位系统是4字节64位系统是8字节 # 这决定了很多数据结构的布局 AC_SYS_LARGEFILE # 翻译检查系统是否支持大文件2GB # # 第七部分生成输出文件 # AC_CONFIG_HEADERS([config.h]) # 翻译把所有检测结果写入 config.h AC_CONFIG_FILES([ Makefile mono/Makefile mono/mini/Makefile mono/metadata/Makefile ]) # 翻译根据检测结果从 .in 模板生成这些Makefile AC_OUTPUT # 翻译执行输出生成所有文件每个AC_宏背后发生了什么让我们深入一个看似简单的宏——AC_CHECK_HEADERS([sys/mman.h])——看看它展开后到底做了什么。当Autoconf处理这个宏时它会生成这样一段Shell脚本嵌入到最终的configure脚本中# configure 脚本中的实际代码简化版echo-nchecking for sys/mman.h... # 创建一个临时C源文件catconftest.cEOF #include sys/mman.h int main() { return 0; } EOF# 尝试编译它if$CC-cconftest.c-oconftest.o2/dev/null;thenechoyes# 编译成功说明这个头文件存在# 在config.h中添加宏定义echo#define HAVE_SYS_MMAN_H 1config.helseechono# 编译失败说明这个头文件不存在echo/* #undef HAVE_SYS_MMAN_H */config.hfi# 清理临时文件rm-fconftest.c conftest.o看到了吗它的检测方法简单粗暴——直接写一个小程序尝试编译看能不能通过。能编译 → 说明这个头文件/函数/库存在。编译失败 → 说明不存在。这就像检测员检查墙体能不能打孔——他不会去查图纸他直接拿电钻试一下。钻得动就是砖墙钻不动就是钢筋混凝土。简单但有效。AC_CHECK_LIB([pthread], [pthread_create])的检测方式更有意思echo-nchecking for pthread_create in -lpthread... catconftest.cEOF extern int pthread_create(); int main() { pthread_create(); return 0; } EOF# 尝试编译并链接加上 -lpthreadif$CCconftest.c-oconftest-lpthread2/dev/null;thenechoyes# 链接成功pthread库存在LIBS$LIBS-lpthreadelseechono# 链接失败pthread库不存在或不在标准路径firm-fconftest.c conftest它不仅编译还尝试链接。因为头文件存在不代表库文件也存在——你可能装了开发头文件但没装库或者库的版本不对。第三章Autoconf的工作过程——从宏到脚本M4宏展开引擎Autoconf的核心引擎是M4——一个古老的宏处理器1977年诞生比很多程序员的父母都老。M4的工作原理很简单文本替换。你定义一个宏M4就把所有出现这个宏的地方替换成宏的定义内容。输入AC_CHECK_HEADERS([sys/mman.h]) M4查找AC_CHECK_HEADERS的定义发现它被定义为一大段Shell脚本模板。 M4把 [sys/mman.h] 作为参数代入模板。 输出一段完整的Shell脚本就是上面那段检测代码。Autoconf预定义了几百个AC_开头的宏每个宏展开后都是一段精心编写的Shell脚本。这些脚本处理了各种边界情况和系统差异。整个展开过程就像俄罗斯套娃configure.ac你写的几百行 ↓ autoconfM4宏展开 configure生成的几万行几百行的configure.ac展开后变成几万行的configure脚本。这就像你写了一份简短的检测清单“检查电路、检查水管、检查燃气”。检测员拿到清单后展开成一本厚厚的操作手册——检查电路要分几步、用什么工具、怎么判断合格、不合格怎么处理……每一项都有详细的操作规程。实际执行autoconf# 在Mono源码目录下执行autoconf# 或者通过autogen.sh间接调用./autogen.sh执行后目录下会多出一个configure文件ls-laconfigure -rwxr-xr-x1user user1847293Jun1510:30 configure1.8MB将近5万行Shell脚本。这个文件是完全自包含的——它不依赖Autoconf不依赖M4不依赖任何特殊工具。它只需要一个基本的Shell/bin/sh就能运行。这是Autoconf的一个重要设计哲学生成的configure脚本必须能在最简陋的系统上运行。因为configure的使用者编译软件的人不一定安装了Autoconf。只有软件的开发者才需要Autoconf。开发者的机器需要 autoconf生成configure 用户的机器 只需要 sh运行configure第四章configure脚本的运行——检测员出动运行configure./configure--prefix/usr/local/mono然后你会看到屏幕上飞速滚过大量的检测信息checking build system type... x86_64-pc-linux-gnu checking host system type... x86_64-pc-linux-gnu checking for gcc... gcc checking whether the C compiler works... yes checking for C compiler default output file name... a.out checking for suffix of executables... checking whether we are cross compiling... no checking for suffix of object files... o checking whether the compiler supports -g... yes checking for gcc option to enable C11 features... none needed checking for g... g checking whether the C compiler works... yes checking for a BSD-compatible install... /usr/bin/install -c checking for sys/mman.h... yes checking for sys/socket.h... yes checking for pthread.h... yes checking for strerror... yes checking for mmap... yes checking for dlopen... no checking for dlopen in -ldl... yes checking for pkg-config... /usr/bin/pkg-config checking for GLIB... yes checking size of void *... 8 checking whether byte ordering is bigendian... no ...每一行checking for XXX... yes/no都是一次检测。整个过程就像一个极其严谨的检测员在逐项检查检测员configure的工作日志 09:00 到达工地开始运行 09:01 确认地址x86_64-pc-linux-gnu确认操作系统和CPU架构 09:02 找到了施工队长gcc找到C编译器 09:03 让施工队长试砌了一面墙成功编译器能正常工作 09:04 检查工具箱里有没有电钻有检查某个头文件 09:05 检查工具箱里有没有水平仪有检查另一个头文件 09:06 检查仓库里有没有水泥没有某个库不存在 09:07 去隔壁仓库找找到了在另一个路径找到了库 09:08 测量层高8字节64位系统指针大小8字节 09:09 检查电路方向小端序x86的字节序 ... 10:30 检测完毕生成报告生成config.h和Makefile生成的config.h检测完成后configure生成一个config.h文件里面记录了所有检测结果/* config.h - 由configure自动生成不要手动修改 *//* 系统特性 */#defineSIZEOF_VOID_P8/* 指针大小8字节64位 */#defineWORDS_BIGENDIAN0/* 字节序小端 *//* 头文件 */#defineHAVE_SYS_MMAN_H1/* 有 sys/mman.h */#defineHAVE_SYS_SOCKET_H1/* 有 sys/socket.h */#defineHAVE_PTHREAD_H1/* 有 pthread.h */#defineHAVE_UNISTD_H1/* 有 unistd.h *//* #undef HAVE_SYS_EPOLL_H *//* 没有 sys/epoll.h可能是macOS *//* 函数 */#defineHAVE_STRERROR1/* 有 strerror() */#defineHAVE_MMAP1/* 有 mmap() */#defineHAVE_DLOPEN1/* 有 dlopen() */#defineHAVE_GETPAGESIZE1/* 有 getpagesize() *//* #undef HAVE_KQUEUE *//* 没有 kqueue()可能是Linux *//* 库 */#defineHAVE_LIBPTHREAD1/* 有 pthread 库 */#defineHAVE_LIBDL1/* 有 dl 库 *//* 包信息 */#definePACKAGEmono#defineVERSION6.12.0然后Mono的源码就可以这样使用这些宏// mono/mini/mini-posix.c#includeconfig.h// 引入检测结果#ifdefHAVE_SYS_MMAN_H#includesys/mman.hvoid*allocate_code_memory(size_tsize){// 使用mmap分配可执行内存用于JIT编译后的机器码returnmmap(NULL,size,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);}#elsevoid*allocate_code_memory(size_tsize){// 没有mmap退而求其次用malloc// 但这样分配的内存可能不可执行某些平台会有问题returnmalloc(size);}#endif#ifdefHAVE_DLOPEN#includedlfcn.hvoid*load_native_library(constchar*name){returndlopen(name,RTLD_LAZY);}#elifdefined(HOST_WIN32)void*load_native_library(constchar*name){returnLoadLibrary(name);}#else#errorNo dynamic library loading support!#endif看到了吗config.h是连接系统检测和条件编译的桥梁。Autoconf检测系统环境 → 结果写入config.h→ 源码根据config.h中的宏选择正确的代码路径。第五章为什么不直接写configure脚本你可能会问既然最终需要的是configure脚本为什么不直接手写它为什么要多一层Autoconf三个原因第一复杂度。一个AC_CHECK_HEADERS([sys/mman.h])宏展开后是几十行Shell脚本。这几十行脚本处理了各种边界情况——编译器输出重定向、临时文件清理、交叉编译支持、缓存机制、日志记录……你手写的话大概率会漏掉某些情况。Autoconf的宏是经过几十年、几百万个项目验证的它们处理了你想不到的各种怪异系统行为。第二可移植性。不同系统的Shell有微妙的差异。Bash的语法和POSIX sh的语法不完全一样。Solaris的/bin/sh和Linux的/bin/sh行为不同。Autoconf生成的脚本严格遵循POSIX标准确保在任何系统上都能运行。第三可维护性。configure.ac是声明式的——你只说我需要检查什么不说怎么检查。这让它简洁、易读、易维护。configure.ac 几百行声明式人类可读 configure 几万行过程式机器生成你维护几百行的清单还是维护几万行的脚本答案不言而喻。尾声一个时代的基石Autoconf诞生于1991年距今已经三十多年了。在这三十多年里它帮助了无数开源项目实现了跨平台编译——Linux内核的工具链、GCC编译器、Python解释器、Apache服务器、OpenSSL加密库、FFmpeg多媒体框架……以及我们今天的主角Mono运行时。它不性感不时髦没有华丽的GUI没有花哨的功能。它只是一个老老实实的检测员拿着清单逐项检查生成报告。但正是这个不起眼的检测员让一份源码能在几十种不同的操作系统上编译运行。它让开发者可以专注于写代码而不是跟每个系统的怪癖搏斗。它让一次编写到处编译从口号变成了现实。下次你执行./configure make make install的时候当你看到屏幕上那些checking for XXX... yes飞速滚过请记住——那是一个三十多岁的检测员正在一丝不苟地检查你的系统确保接下来的每一块砖都能砌在正确的位置上。它从不出错。它从不抱怨。它只是安静地做好自己的工作然后把舞台让给真正的主角——你的代码。