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 )。
def configure_file(input_file, output_file, vars_dict):
with input_file.open('r') as f:
template = f.read()
for var in vars_dict:
template = template.replace('@' + var + '@', vars_dict[var])
with output_file.open('w') as f:
f.write(template)
这个函数读取一个输入文件,遍历vars_dict
变量中的目录,并用对应的值替换@key@
,再将结果写入输出文件。这里的键值对,将由CMake提供。
准备工作
print_info.c.in
和example.f90
与之前的示例相同。此外,我们将使用Python脚本configuration.py
,它提供了一个函数:
def configure_file(input_file, output_file, vars_dict):
with input_file.open('r') as f:
template = f.read()
for var in vars_dict:
template = template.replace('@' + var + '@', vars_dict[var])
with output_file.open('w') as f:
f.write(template)
该函数读取输入文件,遍历vars_dict
字典的所有键,用对应的值替换模式@key@
,并将结果写入输出文件(键值由CMake提供)。
具体实施
与前面的示例类似,我们需要配置一个模板文件,但这一次,使用Python脚本模拟configure_file()
函数。我们保持CMakeLists.txt基本不变,并提供一组命令进行替换操作configure_file(print_info.c.in print_info.c @ONLY)
,接下来将逐步介绍这些命令:
首先,构造一个变量
_config_script
,它将包含一个Python脚本,稍后我们将执行这个脚本:set(_config_script
"
from pathlib import Path
source_dir = Path('${CMAKE_CURRENT_SOURCE_DIR}')
binary_dir = Path('${CMAKE_CURRENT_BINARY_DIR}')
input_file = source_dir / 'print_info.c.in'
output_file = binary_dir / 'print_info.c'
import sys
sys.path.insert(0, str(source_dir))
from configurator import configure_file
vars_dict = {
'_user_name': '${_user_name}',
'_host_name': '${_host_name}',
'_fqdn': '${_fqdn}',
'_processor_name': '${_processor_name}',
'_processor_description': '${_processor_description}',
'_os_name': '${_os_name}',
'_os_release': '${_os_release}',
'_os_version': '${_os_version}',
'_os_platform': '${_os_platform}',
'_configuration_time': '${_configuration_time}',
'CMAKE_VERSION': '${CMAKE_VERSION}',
'CMAKE_GENERATOR': '${CMAKE_GENERATOR}',
'CMAKE_Fortran_COMPILER': '${CMAKE_Fortran_COMPILER}',
'CMAKE_C_COMPILER': '${CMAKE_C_COMPILER}',
}
configure_file(input_file, output_file, vars_dict)
")
使用
find_package
让CMake使用Python解释器:find_package(PythonInterp QUIET REQUIRED)
如果找到Python解释器,则可以在CMake中执行
_config_script
,并生成print_info.c
文件:execute_process(
COMMAND
${PYTHON_EXECUTABLE} "-c" ${_config_script}
)
之后,定义可执行目标和依赖项,这与前一个示例相同。所以,得到的输出没有变化。
工作原理
回顾一下对CMakeLists.txt的更改。
我们执行了一个Python脚本生成print_info.c
。运行Python脚本前,首先检测Python解释器,并构造Python脚本。Python脚本导入configure_file
函数,我们在configuration.py
中定义了这个函数。为它提供用于读写的文件位置,并将其值作为键值对。
此示例展示了生成配置的另一种方法,将生成任务委托给外部脚本,可以将配置报告编译成可执行文件,甚至库目标。我们在前面的配置中认为的第一种方法更简洁,但是使用本示例中提供的方法,我们可以灵活地使用Python(或其他语言),实现任何在配置时间所需的步骤。使用当前方法,我们可以通过脚本的方式执行类似cmake_host_system_information()
的操作。
但要记住,这种方法也有其局限性,它不能在构建时重新生成print_info.c
的自动依赖项。下一个示例中,我们应对这个挑战。
更多信息
我们可以使用get_cmake_property(_vars VARIABLES)
来获得所有变量的列表,而不是显式地构造vars_dict
(这感觉有点重复),并且可以遍历_vars
的所有元素来访问它们的值:
get_cmake_property(_vars VARIABLES)
foreach(_var IN ITEMS ${_vars})
message("variable ${_var} has the value ${${_var}}")
endforeach()
使用这种方法,可以隐式地构建vars_dict
。但是,必须注意转义包含字符的值,例如:;
, Python会将其解析为一条指令的末尾。