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

Leave a Reply

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

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