6.2 使用Python在配置时生成源码

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-6/recipe-02 中找到,其中包含一个Fortran/C例子。该示例在CMake 3.10版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows(使用MSYS Makefile)上进行过测试。

本示例中,我们将再次从模板print_info.c.in生成print_info.c。但这一次,将假设CMake函数configure_file()没有创建源文件,然后使用Python脚本模拟这个过程。当然,对于实际的项目,我们可能更倾向于使用configure_file(),但有时使用Python生成源代码的需要时,我们也应该知道如何应对。

这个示例有严重的限制,不能完全模拟configure_file()。我们在这里介绍的方法,不能生成一个自动依赖项,该依赖项将在构建时重新生成print_info.c。换句话说,如果在配置之后删除生成的print_info.c,则不会重新生成该文件,构建也会失败。要正确地模拟configure_file(),需要使用add_custom_command()add_custom_target()。我们将在第3节中使用它们,来克服这个限制。

这个示例中,我们将使用一个简单的Python脚本。这个脚本将读取print_info.c.in。用从CMake传递给Python脚本的参数替换文件中的占位符。对于更复杂的模板,我们建议使用外部工具,比如Jinja(参见http://jinja.pocoo.org )。

  1. def configure_file(input_file, output_file, vars_dict):
  2. with input_file.open('r') as f:
  3. template = f.read()
  4. for var in vars_dict:
  5. template = template.replace('@' + var + '@', vars_dict[var])
  6. with output_file.open('w') as f:
  7. f.write(template)

这个函数读取一个输入文件,遍历vars_dict变量中的目录,并用对应的值替换@key@,再将结果写入输出文件。这里的键值对,将由CMake提供。

准备工作

print_info.c.inexample.f90与之前的示例相同。此外,我们将使用Python脚本configurator.py,它提供了一个函数:

  1. def configure_file(input_file, output_file, vars_dict):
  2. with input_file.open('r') as f:
  3. template = f.read()
  4. for var in vars_dict:
  5. template = template.replace('@' + var + '@', vars_dict[var])
  6. with output_file.open('w') as f:
  7. f.write(template)

该函数读取输入文件,遍历vars_dict字典的所有键,用对应的值替换模式@key@,并将结果写入输出文件(键值由CMake提供)。

具体实施

与前面的示例类似,我们需要配置一个模板文件,但这一次,使用Python脚本模拟configure_file()函数。我们保持CMakeLists.txt基本不变,并提供一组命令进行替换操作configure_file(print_info.c.in print_info.c @ONLY),接下来将逐步介绍这些命令:

  1. 首先,构造一个变量_config_script,它将包含一个Python脚本,稍后我们将执行这个脚本:

    1. set(_config_script
    2. "
    3. from pathlib import Path
    4. source_dir = Path('${CMAKE_CURRENT_SOURCE_DIR}')
    5. binary_dir = Path('${CMAKE_CURRENT_BINARY_DIR}')
    6. input_file = source_dir / 'print_info.c.in'
    7. output_file = binary_dir / 'print_info.c'
    8. import sys
    9. sys.path.insert(0, str(source_dir))
    10. from configurator import configure_file
    11. vars_dict = {
    12. '_user_name': '${_user_name}',
    13. '_host_name': '${_host_name}',
    14. '_fqdn': '${_fqdn}',
    15. '_processor_name': '${_processor_name}',
    16. '_processor_description': '${_processor_description}',
    17. '_os_name': '${_os_name}',
    18. '_os_release': '${_os_release}',
    19. '_os_version': '${_os_version}',
    20. '_os_platform': '${_os_platform}',
    21. '_configuration_time': '${_configuration_time}',
    22. 'CMAKE_VERSION': '${CMAKE_VERSION}',
    23. 'CMAKE_GENERATOR': '${CMAKE_GENERATOR}',
    24. 'CMAKE_Fortran_COMPILER': '${CMAKE_Fortran_COMPILER}',
    25. 'CMAKE_C_COMPILER': '${CMAKE_C_COMPILER}',
    26. }
    27. configure_file(input_file, output_file, vars_dict)
    28. ")
  2. 使用find_package让CMake使用Python解释器:

    1. find_package(PythonInterp QUIET REQUIRED)
  3. 如果找到Python解释器,则可以在CMake中执行_config_script,并生成print_info.c文件:

    1. execute_process(
    2. COMMAND
    3. ${PYTHON_EXECUTABLE} "-c" ${_config_script}
    4. )
  4. 之后,定义可执行目标和依赖项,这与前一个示例相同。所以,得到的输出没有变化。

工作原理

回顾一下对CMakeLists.txt的更改。

我们执行了一个Python脚本生成print_info.c。运行Python脚本前,首先检测Python解释器,并构造Python脚本。Python脚本导入configure_file函数,我们在configurator.py中定义了这个函数。为它提供用于读写的文件位置,并将其值作为键值对。

此示例展示了生成配置的另一种方法,将生成任务委托给外部脚本,可以将配置报告编译成可执行文件,甚至库目标。我们在前面的配置中认为的第一种方法更简洁,但是使用本示例中提供的方法,我们可以灵活地使用Python(或其他语言),实现任何在配置时间所需的步骤。使用当前方法,我们可以通过脚本的方式执行类似cmake_host_system_information()的操作。

但要记住,这种方法也有其局限性,它不能在构建时重新生成print_info.c的自动依赖项。下一个示例中,我们应对这个挑战。

更多信息

我们可以使用get_cmake_property(_vars VARIABLES)来获得所有变量的列表,而不是显式地构造vars_dict(这感觉有点重复),并且可以遍历_vars的所有元素来访问它们的值:

  1. get_cmake_property(_vars VARIABLES)
  2. foreach(_var IN ITEMS ${_vars})
  3. message("variable ${_var} has the value ${${_var}}")
  4. endforeach()

使用这种方法,可以隐式地构建vars_dict。但是,必须注意转义包含字符的值,例如:;, Python会将其解析为一条指令的末尾。