我在编写dangjinghao/dcc时尝试完全使用cmake作为构建和测试系统以熟悉cmake。
根据使用来看,cmake明显更适合作为普通应用的构建和测试系统,而编译器这种相对独特的测试方式:
- 运行编译产物
- 将单独的文件作为输入
- 运行编译产物所生成的产物并判断是否正常退出
对于cmake默认的测试的设计来说可能不是天然适配。相比之下,gmake更能轻松实现这种需求。但是这不代表cmake不能这么做。接下来我会简单分享一下我使用cmake构建和测试多stage编译器的方法。以下主要以stage2为例,实际上我的项目实现了stage1~stage3的编译和测试。
自举编译
想要实现自举编译,关键思路是:
- 通过使用编译产物(编译器程序)生成各模块的object file
- 通过
add_executable命令将产生的object file添加到一个可执行的目标。 - 由cmake负责链接过程产生可执行目标
这样可以避免硬编码路径、向cmake声明编译出来的编译器目标、获得来自cmake自动配置的并行化源码编译能力。
其中,最关键的就是add_custom_command指令。它具有OUTPUT和DEPENDS选项,可以声明这条指令执行后的产物与依赖,实现类似gmake的“如果依赖文件较旧则重新执行指令生成新的产物”的效果。以下为一个示例:
1 | |
值得注意的是,DCC_STAGE2_ALL_SOURCES为顶层CMakeLists创建的变量,它包含了编译器的所有源码。compiler_old即现有的编译出来的编译器。通过依赖${compiler_old}和"${CMAKE_SOURCE_DIR}/${src}"实现“当编译器版本更新或者源码更新后触发重新编译”的功能。
接下来需要新增一个目标:
1 | |
其中,set_target_properties的LINKER_LANGUAGE是必要的。而设置RUNTIME_OUTPUT_DIRECTORY的主要作用为了:我的编译器会导入编译器特定的头文件来兼容环境,这是一个相对于编译器程序所在目录的../include/目录,也就是项目根目录下的include目录(此处假设了构建目录位于/build/)。而stage2的CMakeLists.txt位于/stage2/子目录,由顶层CMakeLists导入,会在构建目录下创建对应子目录,并在默认情况下在这里生成目标。这样会导致编译器找不到../include/目录,进而无法正常编译。
接下来再通过add_dependencies(${compiler_new} ${compiler_old})指定目标之间的依赖关系,以满足编译stage2编译器需要在编译stage1编译器后进行的需要。
使用cmake做测试
此处的测试主要为一些短小的测试特定功能的代码。他们都是chibicc风格的测试代码:使用类assert方式进行判断,只要测试正常退出,则说明测试通过。
核心点是通过使用add_custom_command构建测试,再用add_test添加对应的执行命令即可。
1 | |
其中的label变量用于区分不同stage,compiler_target用于指定用哪个编译器目标编译,all_exes用于随后创建一个编译所有测试用例的目标:
1 | |
这样,当运行cmake --build build --target test_stage1时,就会使用stage1的编译器编译所有测试用例并使用ctest并行化运行测试。
如果想看完整实现,可参考dangjinghao/dcc。