My CMake Tutorial 3

我的风格其实极其诡异,因为我只想讲我乐意讲的,但保证实用性,除了第一次的类hello world,全部脚本都在实际项目中使用(当然很多是fcitx的 😛 )。学习曲线什么的,谁管啊。

如果你想找前面的内容,请猛击 https://www.csslayer.info/wordpress/category/cmake/

顺带一提,本人近期一直占据oholh cmake 语言提交 http://www.ohloh.net/languages/74 排行中的一名。(有段生猛的时间也挤进过C语言的排名……不过现在出来了 :P)

本次大容量,包括三个内容,创建自己的可以被find_package使用的CMake脚本,复杂自定义文件生成,Gettext整合。

1、创建自己的可以被find_package使用的CMake脚本

用过 autotools的,如果你没用过 pkg-config,那说明你经验不够丰富……(好吧)

pkg-config是*nix上广泛使用的用于查找库,头文件的统一管理的工具。当然能力有限。下面来介绍一下如何在CMake中引用其他的库文件。

首先出场的就是 find_package 命令。find_package 一般使用方式为

find_package(PackageName [REQUIRED])

按照具体的包的不同还可以添加参数。CMake内置了不少库的查找方式,例如Libxml,X11,Qt,但远远不能满足人们的需求。但CMake也有PkgConfig的支持。下面以寻找glib为例。

如果你仅仅是在自己的项目内使用,你可以创建一个文件夹,比如叫做cmake,然后加入以下命令:

set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})

从而让cmake寻找find_package的脚本的时候也会使用你自己的脚本。脚本的名称为 Find<PackageName>.cmake

如果你想让系统中其他的cmake使用你自己的脚本,就像创建pkg-config一样,那么你可以创建一个名为<PackageName>Config.cmake 的脚本,并安装到 /usr/share/cmake 下面。

# - Try to find the GLIB2 libraries
# Once done this will define
#
#  GLIB2_FOUND - system has glib2
#  GLIB2_INCLUDE_DIR - the glib2 include directory
#  GLIB2_LIBRARIES - glib2 library

# Copyright (c) 2008 Laurent Montel, <montel@kde.org>
#
# Redistribution and use is allowed according to the terms of the BSD license.
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.

if(GLIB2_INCLUDE_DIR AND GLIB2_LIBRARIES)
    # Already in cache, be silent
    set(GLIB2_FIND_QUIETLY TRUE)
endif(GLIB2_INCLUDE_DIR AND GLIB2_LIBRARIES)

find_package(PkgConfig)
pkg_check_modules(PC_LibGLIB2 QUIET glib-2.0)

find_path(GLIB2_MAIN_INCLUDE_DIR
          NAMES glib.h
          HINTS ${PC_LibGLIB2_INCLUDEDIR}
          PATH_SUFFIXES glib-2.0)

find_library(GLIB2_LIBRARY
             NAMES glib-2.0
             HINTS ${PC_LibGLIB2_LIBDIR}
)

set(GLIB2_LIBRARIES ${GLIB2_LIBRARY})

# search the glibconfig.h include dir under the same root where the library is found
get_filename_component(glib2LibDir "${GLIB2_LIBRARIES}" PATH)

find_path(GLIB2_INTERNAL_INCLUDE_DIR glibconfig.h
          PATH_SUFFIXES glib-2.0/include
          HINTS ${PC_LibGLIB2_INCLUDEDIR} "${glib2LibDir}" ${CMAKE_SYSTEM_LIBRARY_PATH})

set(GLIB2_INCLUDE_DIR "${GLIB2_MAIN_INCLUDE_DIR}")

# not sure if this include dir is optional or required
# for now it is optional
if(GLIB2_INTERNAL_INCLUDE_DIR)
  set(GLIB2_INCLUDE_DIR ${GLIB2_INCLUDE_DIR} "${GLIB2_INTERNAL_INCLUDE_DIR}")
endif(GLIB2_INTERNAL_INCLUDE_DIR)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GLIB2  DEFAULT_MSG  GLIB2_LIBRARIES GLIB2_MAIN_INCLUDE_DIR)

mark_as_advanced(GLIB2_INCLUDE_DIR GLIB2_LIBRARIES)

脚本开头的第一件事就和c语言中的ifdef差不多,不要找两次。然后引用PkgConfig,从而使用PkgConfig相关的宏。接下来使用PkgConfig中的宏来找glib-2.0.pc,然后会生成以第一个参数为前缀的一系列变量。之后用find_library定位具体的库的位置。find_path来找一些额外头文件的位置。

最后采用FindPackageHandleStandardArgs里面的宏来显示一些默认信息。mark_as_advanced的目的是不在cmake-gui中显示某些变量。

当然你也可以直接在CMakeLists.txt 中使用PkgConfig宏包里面的命令,但创建可以复用的CMake脚本会更好。顺带一说,KDE在/usr/share/apps/cmake/下面有大量的其他库的find_package脚本。如果找不到的话也可以去那里找找。

2、复杂的自定义文件生成

我比较喜欢的方式是,用add_custom_command来指定文件的生成规则,用add_custom_target来确定是否要在make all中执行。

例如fcitx下载词库的部分。

add_custom_target(pinyin_data ALL DEPENDS ${PY_DATA})

add_custom_command(OUTPUT ${PY_ORGDATA}
                   COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/download.sh "${WGET}" "${PY_URL}" "${PY_TAR}"
                   COMMAND ${TAR} xzmvf ${PY_TAR})
add_custom_command(OUTPUT ${PY_DATA}
                   DEPENDS ${PY_ORGDATA}
                   COMMAND createPYMB ${CMAKE_CURRENT_SOURCE_DIR}/gbkpy.org ${CMAKE_CURRENT_BINARY_DIR}/${PY_ORGDATA})

创建了一个名为 pinyin_data的target,DEPENDS后面指定了依赖的文件或者target。ALL表示总是执行。因为custom_target其实也可以指定输出,来实现按需编译。

然后是下载,下载这里我特意用了一个脚本来下载,因为我不想把下载的文件通过make clean清除掉。OUTPUT中的文件是会被make clean删除的。COMMAND可以指定多个命令,像脚本中那样。

第二个命令用来执行createPYMB,并且生成文件。

那么make的时候是个什么规则呢,首先是由于第一行脚本,make all也会触发make pinyin_data。然后make pinyin_data依赖的是${PY_DATA}的内容。于是${PY_DATA}就又会查看它所依赖的文件,${PY_ORGDATA}是否存在。没有则执行第一个custom_command,然后再执行第二个,从而完成pinyin_data这个target。

Lex,Yacc等可以通过类似方式(当然你可以自己写个宏,来简化命令)。

3、Gettext 整合

这年头,如果你的项目不支持国际化你都不好意思出来见人。那么用cmake来使用Gettext如何呢?

让我们从第一步开始。

1) 从po文件生成mo文件。

这是最简单的一步

首先引用cmake自带的Gettext支持。

find_package(Gettext REQUIRED)

然后在你的po目录下。

set(POT_FILE
    kcm_fcitx.pot
)

file(GLOB PO_FILES
    *.po
)

# Update .po files and compile them to binary .gmo files
gettext_create_translations(${POT_FILE} ALL ${PO_FILES})

第一个是设定变量。

第二个是将文件名变成字符串变量,具体来说,就是满足*.po文件名的文件,变成PO_FILES这个变量的内容。前面的GLOB用于指定这个文件名匹配功能,当然还有很多其他的命令。这样最后就能将 *.mo 文件生成,并且install命令都可以省略了。

2) 自动提取源代码中的字符串到pot

我使用过的有两种风格,一种是KDE偏好的,一种是我自己编好的。KDE喜欢用的是使用Messages.sh 这个脚本来提取,而如果你本身就是KDE的项目,则会有机器人每天自动执行Messages.sh来提取。我在kcm-fcitx 中使用了这种风格,既然是shell脚本里面做什么就随便了:

https://github.com/fcitx/kcm-fcitx/blob/master/po/Messages.sh

可以自行修改来符合自身需要。

另外KDE给机器人使用的Messages.sh是不一样的,请自行留意。例如:

http://quickgit.kde.org/?p=kdeplasma-addons.git&a=blob&f=applets/kimpanel/src/Messages.sh

我自己风格的就是完全按照fcitx的需要设计的,因为fcitx需要提取的字符串更多更复杂。另外我这种方式也更适合使用intltool的人来迁移。

http://code.google.com/p/fcitx/source/browse/po/POTFILES.in.in

由于intltool本身的限制,所以里面使用了比较复杂的trick。

file(RELATIVE_PATH REL_SOURCE_ROOT ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR})
if ("${REL_SOURCE_ROOT}" STREQUAL "")
    set(REL_SOURCE_ROOT ".")
endif("${REL_SOURCE_ROOT}" STREQUAL "")

set(POT_FILE fcitx.pot)

configure_file(POTFILES.in.in ${CMAKE_CURRENT_BINARY_DIR}/POTFILES.in)

extract_fcitx_addon_conf_postring()

add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/desc.po
                   COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/getdescpo ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}
                   DEPENDS getdescpo)

add_custom_target(
    pot
    COMMAND INTLTOOL_EXTRACT=${INTLTOOL_EXTRACT} srcdir=${CMAKE_CURRENT_BINARY_DIR} ${INTLTOOL_UPDATE} --gettext-package fcitx --pot
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/conf.po ${CMAKE_CURRENT_BINARY_DIR}/desc.po
    )

首先是获得项目编译目录和源码目录的“相对路径”。并将其填入REL_SOURCE_ROOT中,如果是同一个目录那么改成 “.”。

其次使用configure_file命令生成全部是相对路径的POTFILES.in,configure_file的使用参见第二次的Tutorial。

然后是一个fcitx的宏,跳过。

然后是一个fcitx需要的target,跳过。

最后是一个target,由于它没有被人依赖,所以make all并不会执行它。它的目的是更新“源码目录”(并非编译目录)下的pot文件。

里面的INTLTOOL_EXTRACT是采用

FIND_PROGRAM(INTLTOOL_EXTRACT intltool-extract)
FIND_PROGRAM(INTLTOOL_UPDATE intltool-update)
FIND_PROGRAM(INTLTOOL_MERGE intltool-merge)

获得的。find_program可以找系统的Path下面的可执行文件,也可以用其他参数来指定搜索路径。

DEPENDS里面的内容可以忽略,依赖的其实就是刚刚跳过的两个命令的OUTPUT。

3) 用intltool 合并desktop 翻译

    ADD_CUSTOM_COMMAND(
        OUTPUT ${outfile}
        COMMAND LC_ALL=C ${INTLTOOL_MERGE} -d -u ${PROJECT_SOURCE_DIR}/po ${infile} ${outfile}
        DEPENDS ${infile}
    )

默认指定了目录和格式。一个简单的CUSTOM_COMMAND,通过刚刚的学习,想必大家都知道这里做了什么事情吧。

4) 非Linux平台的gettext库的查找

用此脚本即可,方法参见本次第一节:

http://code.google.com/p/fcitx/source/browse/cmake/FindLibintl.cmake

This entry was posted in cmake and tagged . Bookmark the permalink.

3 Responses to My CMake Tutorial 3

  1. nihui says:
    Firefox 9.0 Windows 7

    很有用,学习了。感谢分享! ^^:)

    [Reply]
  2. 右京样一 says:
    Google Chrome 16.0.912.75 GNU/Linux

    果然已经基本看不懂了……要看懂这个貌似得有维护较大项目的经验……

    [Reply]
  3. vx13 says:
    Firefox 22.0 Windows 8 x64 Edition

    文章里的链接还是老站的,要不要更新一下链接?

    [Reply]

Leave a Reply

Your email address will not be published. Required fields are marked *

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).

This site uses Akismet to reduce spam. Learn how your comment data is processed.