使用 CMake 进行跨平台软件开发

使用 CMake 进行跨平台软件开发

原文:Cross-Platform Software Development Using CMake http://www.linuxjournal.com/article/6700

作者:Andrej Cedilnik 翻译:宇舟


在每个系统构建你的工程,而无须关心创建可执行文件和动态库的具体方法。

当观察一大堆工程,会发现一件事:对构建过程的描述总是存储在一组文件中。这些文件可能是简单的shell脚本、Makefiles、Jam文件、基于复杂的脚本的工程像Autoconf和Automake。

最近,一个新的玩家 CMake 加入了软件构建游戏。CMake 使用原生构建工具,像 Make 甚至是微软的 Visual Studio,而不直接是一个的构建程序。支持多个平台,in-source 和 out-source 构建,跨库依赖检测,并行构建,可配置的头文件。极大的降低了跨平台软件开发和维护过程的复杂性。

构建系统

观察下大部分软件开发工程,无疑你会面对一个共同的问题。你有一大堆源文件,一些文件依赖于其他的,你想生成最终的二进制文件。有时候你想做更复杂的事,但是在大多数情况下就这样。

你有个小的工程想在你linux上构建。你坐下很快的写出下面的 Makefile:

main.o: main.c main.h
cc -c main.c
MyProgram: main.o
cc -o MyProgram main.o -lm -lz

当文件写好后,你要做的只是输入 make 命令,然后工程被构建。如果任何文件被改变,所有必要的文件会被重新构建。很好,现在你可以祝贺自己并喝一杯。

除非,你的老板过来说:“我们刚得到一个新的XYZ型计算机,你要在上面构建这个软件。”所以,你把文件复制过去,输入 make 命令,并得到下面的错误信息:

cc: Command not found

你知道那个XYZ型计算机上有个编译器叫做 cc-XYZ ,所以修改 Makefile 后重试。但是发现系统没有zlib。所以你去掉 -lz 参数,直接包进zlib的代码,搞定。

就像你看到的,这个问题是当使用 Makefile 时,只要文件移到新的使用不同编译器名字或参数的平台,make失效了。

看个这个问题的更详细的例子。让我们看下我们最爱的压缩库 zlib 。 zlib 是个相当简单的库,由17个C源文件和11个头文件组成。编译zlib是简单的。所有要做的是编译每个C文件然后把他们链接到一起。你可以写个 Makefile 解决他,但是在每个单独平台下,你必须去修改 Makefile 以便可用。

像 Autoconf 和 Automake 之类的工具在UNIX和UNIX类平台下很好的解决了这个问题。但是他们通常太复杂。使事情变的更糟糕了,在大多数工程中,开发者最终要在 Autoconf的输入文件中写shell脚本。结果很快变成依赖于开发者的假设。因为,Autoconf的结果依赖于shell,这些配置文件在没有 Bourne Shell或者类似标准/bin/sh的平台上是无效的。Autoconf 和 Automake 也依赖于几个系统上安装的工具。

CMake 是这些问题的一个解决方案。相对于其他类似工具,CMake 对底层系统做更少的假设。CMake使用标准C++实现,所以他可以在大多数现代操作系统上运行。它不使用除了系统的本地构建工具外的其他的工具。

Installing CMake

在一些平台下,像 Debian GNU/Linux ,CMake 是标准包。对于大多数其他平台,包括UNIX、Mac OS X、Microsof Windows,CMake 二进制包可以直接从 CMake Web site 下载。你可以尝试执行 cmake --help 以检测CMake是否被安装。这个命令会显示CMake版本和使用信息。

Simple CMake

现在CMake安装好了,我们可以在我们的工程中使用他了。我们要先准备CMake的输入文件叫做 CMakeLists.txt 。例如,下面是个简单的 CMakeLists.txt:

PROJECT(MyProject C)
ADD_LIBRARY(MyLibrary STATIC libSource.c)
ADD_EXECUTABLE(MyProgram main.c)
TARGET_LINK_LIBRARIES(MyProgram MyLibrary z m)

使用 CMake 构建工程是极简单的。在包含CMakeLists.txt 的目录中,输入下面的2个命令,path是到源码的路径:

cmake path
make

CMake 读取 CMakeLists.txt 文件从源目录,在当前目录下为系统产生适当的Makefiles。CMake 维护依赖的头文件的列表,所以依赖检测是被确保的。如果你要添加更多的源文件,只要简单的添加到列表中。当 Makefiles 产生后,你不必再执行 CMake ,因为对 CMakeLists.txt 的依赖检测已经添加到产生的 Makefils 中。如果你想确保依赖重新产生,你可以执行 make depend 。

CMake Commands

CMake 本质上是一个简单的解释器。CMake 输入文件有一个极度简单但是强大的语法。它由命令、原始流控制构造、宏和变量组成。所有的命令有完全一样的语法:

COMMAND_NAME(ARGUMENT1 ARGUMENT2 ...)

例如,命令 ADD_LIBRARY 指定一个库应该被创建。第一个参数是库的名字,第二个可选参数是用来指定这个库是静态的还是动态的,其他的参数是源文件的列表。你想要动态库?简单的用 SHARED 替换 STATIC。所有的命令的列表请看 CMake 的文档。

还有一些流控制构造,例如 IF 和 FOREACH 。IF 中的表达式不能包含其他命令,可以使用NOT、AND、OR。下面是一个通常使用 IF 语句的例子:

IF(UNIX)
IF(APPLE)
SET(GUI "Cocoa")
ELSE(APPLE)
SET(GUI "X11")
ENDIF(APPLE)
ELSE(UNIX)
IF(WIN32)
SET(GUI "Win32")
ELSE(WIN32)
SET(GUI "Unknown")
ENDIF(WIN32)
ENDIF(UNIX)
MESSAGE("GUI system is ${GUI}")

这个例子展现了对 IF 语句和变量的使用。

FOREACH 命令的参数,第一个是保存迭代内容的变量,后面的是被迭代的列表。例如,如果有一列可执行文件需要创建,每个可执行文件是被从同名源文件创建,可以像下面这样使用FOREACH:

SET(SOURCES source1 source2 source3)
FOREACH(source ${SOURCES})
ADD_EXECUTABLE(${source} ${source}.c)
ENDFOREACH(source)

使用宏构造可以定义一个宏。当我们要经常创建一些可执行文件并且要链接一些库。下面的宏可以是我们的生活更简单点。在这个例子 中,CREATE_EXECUTABLE 是宏的名字,其他的是参数。在宏内部,所有的参数被看作变量。宏一被创建,即可被当作常规命令使用。CREATE_EXECUTABLE的定义和使用象这 样:

MACRO(CREATE_EXECUTABLE NAME
SOURCES LIBRARIES)
ADD_EXECUTABLE(${NAME} ${SOURCES})
TARGET_LINK_LIBRARIES(${NAME}
${LIBRARIES})
ENDMACRO(CREATE_EXECUTABLE)
ADD_LIBRARY(MyLibrary libSource.c)
CREATE_EXECUTABLE(MyProgram main.c MyLibrary)

宏不等价于一般程序语言中的函数或过程,宏不能被递归调用。

条件编译

好的构建程序的一个重要特性是能将构建的一部分打开或关闭。构建程序也应该能找到和设置好你的工程需要的系统资源的位置。所有这些功能在 CMake 中使用条件编译实现。让我演示一个例子。让我们假设你的工程有2个模式,常规模式和调试模式。调试模式添加一些调试代码到常规代码中。因此,你的代码中充 满这样的代码片段:

#ifdef DEBUG
fprintf(stderr,
"The value of i is: %dn", i);
#endif /* DEBUG */

为了告诉 CMake 添加一个 -DDEBUG 到编译命令中,你可以对属性COMPILE_FLAGS使用SET_SOURCE_FILES_PROPERTIES命令。但是可能你不想每次都通过修改 CMakeLists.txt 文件以在调试模式和常规模式中切换。OPTION命令可以用来创建一个布尔型变量可以用于被在构建工程前设置。前一个例子可以被改进为:

OPTION(MYPROJECT_DEBUG
"Build the project using debugging code"
ON)
IF(MYPROJECT_DEBUG)
SET_SOURCE_FILE_PROPERTIES(
libSource.c main.c
COMPILE_FLAGS -DDEBUG)
ENDIF(MYPROJECT_DEBUG)

现在,你问:“我该怎样设置这个变量?” CMake 带了3个GUI工具。在UNIX类系统中,有一个终端GUI工具叫做ccmake,这是一个基于文本的,可以在一个远程终端连接上使用的。CMake也有微软Windows和Mac OS X版本的GUI工具。

当你在使用CMake产生的Makefiles,如果你已经执行过CMake,现在只要输入命令 make edit_cache 。这个命令会运行一个合适的GUI工具。在所有的GUI工具中,你有一些用于设置变量的选项。就像你看到的,CMake有几个缺省选项,例如 EXECUTABLE_OUTPUT_PATH 、LIBRARY_OUTPUT_PATH(可执行文结合库文件被输出到的路径),在我们前面的例子中还有MYPROJECT_DEBUG。当改变一些变 量的值后,你按下配置按钮或c键(在ccmake中)。

在GUI工具中,你将设置几种不同类型的条目。MYPROJECT_DEBUG是布尔型的,另一种常见的变量类型是路径。假设我们的程序依赖于Pyhon.h文件的位置。我们在CMakeLists.txt文件中加入下面的用于尝试寻找一个文件的命令:

FIND_PATH(PYTHON_INCLUDE_PATH Python.h
/usr/include
/usr/local/include)

因为在每个单独的工程中都去重复的指定所有的位置是浪费的。你可以包含(include)其他叫做模块(modules)的CMakes文件。 CMake自带一些有用模块,从用于搜索不同软件包的到干一些实际的事或定义一些宏的模块。全部的模块列表请看CMake的模块子目录。例如,有个模块叫 做FindPythonLibs.cmake,可以在大多数系统的用于查找Python库文件和头文件路径。然而如果CMake不能找到你要的,你总是可 以在GUI工具中指定。你也可以通过命令行访问CMake变量。下面的一行命令设置MYPROJECT_DEBUG为OFF:

cmake -DMYPROJECT_DEBUG:BOOL=OFF

What about Subdirectories? 子目录?

作为一个软件开发者,你可能要把源代码组织到子目录中。不同的子目录可以代表不同的库、可执行文件、测试、文档。现在我们可以启用或禁用子目录以便 构建工程的一部分跳过另一部分。使用SUBDIRS 命令告诉 CMake 处理一个子目录。这个命令使CMake到指定的子目录下去找 CMakeLists.txt 文件。使用这个命令可以使我们的工程更有组织。我们移动所有的库文件到库子目录中,顶级 CMakeLists.txt 现在看起来像这样:

PROJECT(MyProject C)
SUBDIRS(SomeLibrary)
INCLUDE_DIRECTORIES(SomeLibrary)
ADD_EXECUTABLE(MyProgram main.c)
TARGET_LINK_LIBRARIES(MyProgram MyLibrary)

INCLUDE_DIRECTORIES 命令告诉编译器到哪里去找 main.c 用到的头文件。因此,即使你的工程有500个子目录,你把你的所有源文件都放到子目录中,也不会有依赖问题。CMake帮你都干了。

回到Zlib

现在,我们要个“cmake化”的zlib。从一个简单的 CMakeLists.txt 文件开始:

PROJECT(ZLIB)
# source files for zlib
SET(ZLIB_SRCS
adler32.c gzio.c
inftrees.c uncompr.c
compress.c infblock.c
infutil.c zutil.c
crc32.c infcodes.c
deflate.c inffast.c
inflate.c trees.c
)
ADD_LIBRARY(zlib ${ZLIB_SRCS})
ADD_EXECUTABLE(example example.c)
TARGET_LINK_LIBRARIES(example zlib)

现在你可以构建它了。然而,有些小事要记住。首先,zlib在有些平台需要unistd.h文件。因此,我们加上下面的测试代码:

INCLUDE (
${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake)
CHECK_INCLUDE_FILE(
"unistd.h" HAVE_UNISTD_H)
IF(HAVE_UNISTD_H)
ADD_DEFINITION(-DHAVE_UNISTD_H)
ENDIF(HAVE_UNISTD_H)

在Windows下,我们也必须为共享库做一些而外的事。zlib需要加上-DZLIB_DLL参数编译,以使导出宏正确。因此,我们加上下面的选项:

OPTION(ZLIB_BUILD_SHARED
"Build ZLIB shared" ON)
IF(WIN32)
IF(ZLIB_BUILD_SHARED)
SET(ZLIB_DLL 1)
ENDIF(ZLIB_BUILD_SHARED)
ENDIF(WIN32)
IF(ZLIB_DLL)
ADD_DEFINITION(-DZLIB_DLL)
ENDIF(ZLIB_DLL)

虽然这样也能工作,但是有一个更好的方法。我们可以配置一个头文件以代替传入ZLIB_DLL和HAVE_UNISTD_H。我们要准备一个使用cmake标记的输入文件。下面是zlibConfig.h,一个zlib包含文件的例子:

#ifndef _zlibConfig_h
#define _zlibConfig_h
#cmakedefine ZLIB_DLL
#cmakedefine HAVE_UNISTD_H
#endif

这里,#cmakedefine VAR 被替换为 #define VAR或者#undef VAR,依赖于cmake中VAR是否被定义。我们使用下面的CMake命令让CMake创建文件 zlibConfig.h

CONFIGURE_FILE(
${ZLIB_SOURCE_DIR}/zlibDllConfig.h.in
${ZLIB_BINARY_DIR}/zlibDllConfig.h)

To Infinity and Further

通过这篇文章,你可以开始在你的日常工作中使用CMake。如果你的代码足够可以移植,现在你可以连接到你的朋友的AIX系统上构建你的工程。CMake文件也比Makefiles文件容易理解,因此你的朋友可以检查你的遗漏。

然而,这个例子涉及的比较浅,CMake有能力干许多其他的工作。在1.6版中,你可以做平台独立的TRY_RUN和TRY_COMPILE构建, 以方便的测试系统的能力。CMake原生支持C和C++,但是有限支持构建java文件。通过一些努力,你可以构建任何东西从Python、Emacs脚 本到LaTeX文档。使用CMake作为测试框架驱动,你可以实现平台独立的回归测试。如果你想走的更远,你可以使用CMake的C API为CMake写一个插件,以添加你自己的命令。

CMake is being actively used in several projects such as VTK and ITK. Its benefits are enormous in traditional software development, however they become even more apparent, when portability is necessary. By using CMake for software development, your code will be significantly more "open", because it will build on a variety of platforms.

 

译者注:

        CMake 已经发展到版本2.6了,这篇文章原文是2003写的,当时才1.6。现在已经有了很大改进了。KDE4整个工程文件就是用CMake的。
  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值