15.2 生成文件并编写平台检查

对于Vim示例,我们需要在配置时生成三个文件,src/auto/pathdef.csrc/auto/config.hsrc/auto/osdef.h:

  • pathdef.c:记录安装路径、编译/链接标志、当前用户和主机名
  • config.h:编译系统的环境
  • osdef.h:由src/osdef.sh生成的文件

这种情况相当普遍。需要CMake配置文件,配置时执行一个脚本,执行许多平台检查命令,来生成config.h。特别是,对于那些可移植的项目,平台检查非常普遍。

在原始目录树中,文件在src文件夹下生成。而我们将使用不同的方法:这些文件会生成在build目录中。这样做的原因是生成的文件通常依赖于所选择的选项、编译器或构建类型,我们希望保持同一个源,可以适配多个构建。要在build目录中启用生成,我们必须对生成文件的脚本进行改动。

构造文件

我们将把与生成文件相关的函数集中放在src/autogenerate.cmake中。在定义可执行目标之前,在src/CMakeLists.txt中调用这些函数:

  1. # generate config.h, pathdef.c, and osdef.h
  2. include(autogenerate.cmake)
  3. generate_config_h()
  4. generate_pathdef_c()
  5. generate_osdef_h()
  6. add_executable(vim
  7. main.c
  8. )
  9. # ...

src/autogenerate.cmake中包含了其他检测头文件、函数和库等几个函数:

  1. include(CheckTypeSize)
  2. include(CheckFunctionExists)
  3. include(CheckIncludeFiles)
  4. include(CheckLibraryExists)
  5. include(CheckCSourceCompiles)
  6. function(generate_config_h)
  7. # ... to be written
  8. endfunction()
  9. function(generate_pathdef_c)
  10. # ... to be written
  11. endfunction()
  12. function(generate_osdef_h)
  13. # ... to be written
  14. endfunction()

我们选择了一些用于生成文件的函数,而不是用宏或“裸”CMake代码。在前几章中讨论过的,这是避免了一些问题:

  • 避免多次生成文件,以防多次包含模块。我们可以使用一个包含保护来防止意外地多次运行代码。
  • 保证了对函数中变量范围的完全控制。这避免了这些定义溢出,从而出现变量污染的情况。

根据系统配置预处理宏定义

config.h文件以src/config.h.in为目标所生成的,其中包含根据系统功能配置的预处理标志:

  1. /* Define if we have EBCDIC code */
  2. #undef EBCDIC
  3. /* Define unless no X support found */
  4. #undef HAVE_X11
  5. /* Define when terminfo support found */
  6. #undef TERMINFO
  7. /* Define when termcap.h contains ospeed */
  8. #undef HAVE_OSPEED
  9. /* ... */

生成的src/config.h示例类似如下情况(定义可以根据环境的不同而不同):

  1. /* Define if we have EBCDIC code */
  2. /* #undef EBCDIC */
  3. /* Define unless no X support found */
  4. #define HAVE_X11 1
  5. /* Define when terminfo support found */
  6. #define TERMINFO 1
  7. /* Define when termcap.h contains ospeed */
  8. /* #undef HAVE_OSPEED */
  9. /* ... */

这个页面是一个很好的平台检查示例: https://gitlab.kitware.com/cmake/community/wikis/doc/tutorials/How-To-Write-Platform-Checks

src/configure.ac中,我们可以检查需要执行哪些平台检查,从而来设置相应的预处理定义。

我们将使用#cmakedefine(https://cmake.org/cmake/help/v3.5/command/configure_file.html?highlight=cmakedefine )为了确保不破坏现有的Autotools构建,我们将复制config.h.inconfig.h.cmake.in,并将所有#undef SOME_DEFINITION更改为#cmakedefine SOME_DEFINITION @SOME_DEFINITION@

generate_config_h函数中,先定义两个变量:

  1. set(TERMINFO 1)
  2. set(UNIX 1)
  3. # this is hardcoded to keep the discussion in the book chapter
  4. # which describes the migration to CMake simpler
  5. set(TIME_WITH_SYS_TIME 1)
  6. set(RETSIGTYPE void)
  7. set(SIGRETURN return)
  8. find_package(X11)
  9. set(HAVE_X11 ${X11_FOUND})

然后,我们执行几个类型检查:

  1. check_type_size("int" VIM_SIZEOF_INT)
  2. check_type_size("long" VIM_SIZEOF_LONG)
  3. check_type_size("time_t" SIZEOF_TIME_T)
  4. check_type_size("off_t" SIZEOF_OFF_T)

然后,我们对函数进行循环,检查系统是否能够解析:

  1. foreach(
  2. _function IN ITEMS
  3. fchdir fchown fchmod fsync getcwd getpseudotty
  4. getpwent getpwnam getpwuid getrlimit gettimeofday getwd lstat
  5. memset mkdtemp nanosleep opendir putenv qsort readlink select setenv
  6. getpgid setpgid setsid sigaltstack sigstack sigset sigsetjmp sigaction
  7. sigprocmask sigvec strcasecmp strerror strftime stricmp strncasecmp
  8. strnicmp strpbrk strtol towlower towupper iswupper
  9. usleep utime utimes mblen ftruncate
  10. )
  11. string(TOUPPER "${_function}" _function_uppercase)
  12. check_function_exists(${_function} HAVE_${_function_uppercase})
  13. endforeach()

验证库是否包含特定函数:

  1. check_library_exists(tinfo tgetent "" HAVE_TGETENT)
  2. if(NOT HAVE_TGETENT)
  3. message(FATAL_ERROR "Could not find the tgetent() function. You need to install a terminal library; for example ncurses.")
  4. endif()

然后,我们循环头文件,检查它们是否可用:

  1. foreach(
  2. _header IN ITEMS
  3. setjmp.h dirent.h
  4. stdint.h stdlib.h string.h
  5. sys/select.h sys/utsname.h termcap.h fcntl.h
  6. sgtty.h sys/ioctl.h sys/time.h sys/types.h
  7. termio.h iconv.h inttypes.h langinfo.h math.h
  8. unistd.h stropts.h errno.h sys/resource.h
  9. sys/systeminfo.h locale.h sys/stream.h termios.h
  10. libc.h sys/statfs.h poll.h sys/poll.h pwd.h
  11. utime.h sys/param.h libintl.h libgen.h
  12. util/debug.h util/msg18n.h frame.h sys/acl.h
  13. sys/access.h sys/sysinfo.h wchar.h wctype.h
  14. )
  15. string(TOUPPER "${_header}" _header_uppercase)
  16. string(REPLACE "/" "_" _header_normalized "${_header_uppercase}")
  17. string(REPLACE "." "_" _header_normalized "${_header_normalized}")
  18. check_include_files(${_header} HAVE_${_header_normalized})
  19. endforeach()

然后,我们将CMake选项从转换为预处理定义:

  1. string(TOUPPER "${FEATURES}" _features_upper)
  2. set(FEAT_${_features_upper} 1)
  3. set(FEAT_NETBEANS_INTG ${ENABLE_NETBEANS})
  4. set(FEAT_JOB_CHANNEL ${ENABLE_CHANNEL})
  5. set(FEAT_TERMINAL ${ENABLE_TERMINAL})

最后,我们检查是否能够编译一个特定的代码片段:

  1. check_c_source_compiles(
  2. "
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. int
  6. main ()
  7. {
  8. struct stat st;
  9. int n;
  10. stat(\"/\", &st);
  11. n = (int)st.st_blksize;
  12. ;
  13. return 0;
  14. }
  15. "
  16. HAVE_ST_BLKSIZE
  17. )

然后,使用定义的变量配置src/config.h.cmake.in生成config.h,其中包含generate_config_h函数:

  1. configure_file(
  2. ${CMAKE_CURRENT_LIST_DIR}/config.h.cmake.in
  3. ${CMAKE_CURRENT_BINARY_DIR}/auto/config.h
  4. @ONLY
  5. )

使用路径和编译器标志配置文件

src/pathdef.c.in生成pathdef.c:

  1. #include "vim.h"
  2. char_u *default_vim_dir = (char_u *)"@_default_vim_dir@";
  3. char_u *default_vimruntime_dir = (char_u *)"@_default_vimruntime_dir@";
  4. char_u *all_cflags = (char_u *)"@_all_cflags@";
  5. char_u *all_lflags = (char_u *)"@_all_lflags@";
  6. char_u *compiled_user = (char_u *)"@_compiled_user@";
  7. char_u *compiled_sys = (char_u *)"@_compiled_sys@";

generate_pathdef_c函数在src/pathdef.c.in进行配置。为了简单起见,我们省略了链接标志:

  1. function(generate_pathdef_c)
  2. set(_default_vim_dir ${CMAKE_INSTALL_PREFIX})
  3. set(_default_vimruntime_dir ${_default_vim_dir})
  4. set(_all_cflags "${CMAKE_C_COMPILER} ${CMAKE_C_FLAGS}")
  5. if(CMAKE_BUILD_TYPE STREQUAL "Release")
  6. set(_all_cflags "${_all_cflags} ${CMAKE_C_FLAGS_RELEASE}")
  7. else()
  8. set(_all_cflags "${_all_cflags} ${CMAKE_C_FLAGS_DEBUG}")
  9. endif()
  10. # it would require a bit more work and execute commands at build time
  11. # to get the link line into the binary
  12. set(_all_lflags "undefined")
  13. if(WIN32)
  14. set(_compiled_user $ENV{USERNAME})
  15. else()
  16. set(_compiled_user $ENV{USER})
  17. endif()
  18. cmake_host_system_information(RESULT _compiled_sys QUERY HOSTNAME)
  19. configure_file(
  20. ${CMAKE_CURRENT_LIST_DIR}/pathdef.c.in
  21. ${CMAKE_CURRENT_BINARY_DIR}/auto/pathdef.c
  22. @ONLY
  23. )
  24. endfunction()

配置时执行shell脚本

最后,我们使用以下函数生成osdef.h:

  1. function(generate_osdef_h)
  2. find_program(BASH_EXECUTABLE bash)
  3. execute_process(
  4. COMMAND
  5. ${BASH_EXECUTABLE} osdef.sh ${CMAKE_CURRENT_BINARY_DIR}
  6. WORKING_DIRECTORY
  7. ${CMAKE_CURRENT_LIST_DIR}
  8. )
  9. endfunction()

为了在${CMAKE_CURRENT_BINARY_DIR}/src/auto而不是src/auto中生成osdef.h,我们必须调整osdef.sh以接受${CMAKE_CURRENT_BINARY_DIR}作为命令行参数。

osdef.sh中,我们会检查是否给定了这个参数:

  1. if [ $# -eq 0 ]
  2. then
  3. # there are no arguments
  4. # assume the target directory is current directory
  5. target_directory=$PWD
  6. else
  7. # target directory is provided as argument
  8. target_directory=$1
  9. fi

然后,生成${target_directory}/auto/osdef.h。为此,我们还必须在osdef.sh中调整以下行:

  1. $CC -I. -I$srcdir -
  2. I${target_directory} -E osdef0.c >osdef0.cc