看到陈皓大神写作的《跟我一起写 Makefile》,我也想出一个CMake学习的专栏。
距离我接触CMake已经过了3年,那是我还是研一,不懂得底层编译的事情,因为导师的项目才突然转到这个方向(项目是做工业软件的)。
当初学习的挣扎、难受都还历历在目,我知道自己一定能学会,但是需要时间,我在心里默默立下了一个Flag,学成之后我一定要写出一篇精彩全面的文章来记录,让后来人入门不再那么艰难。
为什么我一直没动笔呢?即使是现在的我也会认为CMake设计到的知识繁杂,尝试过几次都是浅尝辄止,现在工作了,目前是周末,我又想起了这件事,似乎我的写作能力也得到了提升,才鼓起勇气动笔。
这篇文章使用了官网中的文档,运用我在开发中的经验,采用ParaView这个软件的CMake编写习惯来写作。
希望我写的一点点文字能够对大家有用,不至于如同垃圾文章一样占用资源。
CMake是由KitWare公司开发的一款编译工具,Kitware[1]位于美国纽约,其致力于开源软件开发和软件开发服务,其产品和开源项目在计算机视觉、医学成像、科学可视化等领域得到了广泛应用。其中出品的CMake、VTK以及ParaView是对我最熟悉的三款产品。我也从其中ParaView的源代码中学习到了许多开发的精髓,不止一次感叹于其开发人员的专业性。
我认为学习一项技术不能不知道其历史来源,否则就只是使用技术的工具人而已,我想站在科学技术和人文艺术的交叉点思考问题,我从苹果创始人乔布斯那里学来了这一思维方式。
在CMake出来之前,如何实现从代码的文本文件到可执行的二进制文件的呢?如果只有一个源代码文件main.cpp,那么你在Linux平台下执行gcc main.cpp,就会默认生成名为a.out的可执行文件。随着技术的发展,结构化思维被引入到软件工程之中,每一个人只负责某个模块的开发,最后将所有人的代码文件集中到一起编译,生成最后的可执行文件。很明显这个时候你要依赖别人编译的库,这个时候一定会定义调用和编译他人库的规则才可以使用,于是MakeFile出现了,它包含了如何编译一个项目的流程、规则等,目前Linux平台下许多开源软件仍然使用MakeFile,就在你执行make&&make install命令时,就根据MakeFile文件的规则生成二进制文件。
随意打开一个Linux软件的仓库,VIM,可以看到其使用MakeFile定义编译流程。
点击打开MakeFile文件,我截取了部分代码,我相信没有长时间的学习语法,这是很难掌握的,说实话,我在本科阶段上过一门接近底层编译的课程,其中就有MakeFile的学习,但是我是一点没学会,仅仅知道几个命令而已,根本就不知道如何使用MakeFIle来编写适用于大型工程的规则。
indenttest:
cd runtime/indent && \
$(MAKE) clean && \
$(MAKE) test VIM="$(VIM_FOR_INDENTTEST)"
# Executable used for running the syntax tests.
VIM_FOR_SYNTAXTEST = ../../src/vim
syntaxtest:
cd runtime/syntax && \
$(MAKE) clean && \
$(MAKE) test VIMPROG="$(VIM_FOR_SYNTAXTEST)"
我再打开一个使用Cmake的仓库,ParaView,截取部分,其使用了if语句,对于程序员而言是否更加易懂呢?各位看官自行评价。
# Set up our directory structure for output libraries and binaries
include(GNUInstallDirs)
if (NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}")
endif ()
if (NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}")
endif ()
if (NOT CMAKE_ARCHIVE_OUTPUT_DIRECTORY)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}")
endif ()
记得某个计算机科学家曾经说过,软件中你要解决某个问题,那么都可以通过多加一层来实现。
软件的发展也是符合进化论的,软件变的越来越抽象,人们在前人的基础上开发出越来越高层的东西,比如以前的软件是无法在硬件通用的,但是斯托曼自己开发了一个编译器GCC,使得程序员开发的代码使用GCC编译后,都可以在平台上面运行。
CMake的出现是因为什么呢?依我之见,正是时代的发展,往昔MakeFile并不适用于目前大型的软件工程了,学习成本也过高。但是CMake并没有替代MakeFile,它也是在MakeFile上面加了一层,将其语法简化了而已,其实CMake还是会生成MakeFile文件。所以你看,软件的传承就是这样的,站在巨人的肩膀上。
每个程序员的起点都是hello world,我也打算从CMake的hello world开始写。
我的实验环境使用Kali 2023,,只是因为当前的电脑上面刚好有这个虚拟机,十分方便而已,后续可能会演示Visual Studio的使用方式,毕竟咱们很多时候还是用VS来。
如果是在Windows上面安装也是非常简单的,CMake官网下载之后傻瓜式安装即可,我不再赘述CMake - Upgrade Your Software Build System
Linux平台直接apt install cmake -y,或者yum install cmake -y。如果报没有GCC编译器,install安装即可。
┌──(root㉿kidfu)-[~]
└─# apt install cmake
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following package was automatically installed and is no longer required:
libcbor0.8
Use 'apt autoremove' to remove it.
The following additional packages will be installed:
cmake-data libjsoncpp25 librhash0
Suggested packages:
cmake-doc cmake-format elpa-cmake-mode ninja-build
The following NEW packages will be installed:
cmake cmake-data libjsoncpp25 librhash0
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
Need to get 12.7 MB of archives.
After this operation, 48.6 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 https://mirrors.aliyun.com/kali kali-rolling/main amd64 libjsoncpp25 amd64 1.9.5-6 [79.8 kB]
Get:2 https://mirrors.aliyun.com/kali kali-rolling/main amd64 librhash0 amd64 1.4.3-3 [134 kB]
Get:3 https://mirrors.aliyun.com/kali kali-rolling/main amd64 cmake-data all 3.27.7-1 [2,104 kB]
Get:4 https://mirrors.aliyun.com/kali kali-rolling/main amd64 cmake amd64 3.27.7-1 [10.4 MB]
Fetched 12.7 MB in 2s (5,897 kB/s)
Selecting previously unselected package libjsoncpp25:amd64.
(Reading database ... 403674 files and directories currently installed.)
Preparing to unpack .../libjsoncpp25_1.9.5-6_amd64.deb ...
Unpacking libjsoncpp25:amd64 (1.9.5-6) ...
Selecting previously unselected package librhash0:amd64.
Preparing to unpack .../librhash0_1.4.3-3_amd64.deb ...
Unpacking librhash0:amd64 (1.4.3-3) ...
Selecting previously unselected package cmake-data.
Preparing to unpack .../cmake-data_3.27.7-1_all.deb ...
Unpacking cmake-data (3.27.7-1) ...
Selecting previously unselected package cmake.
Preparing to unpack .../cmake_3.27.7-1_amd64.deb ...
Unpacking cmake (3.27.7-1) ...
Setting up libjsoncpp25:amd64 (1.9.5-6) ...
Setting up librhash0:amd64 (1.4.3-3) ...
Setting up cmake-data (3.27.7-1) ...
Setting up cmake (3.27.7-1) ...
Processing triggers for libc-bin (2.37-12) ...
ldconfig: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link
Processing triggers for man-db (2.11.2-3) ...
Processing triggers for kali-menu (2023.4.5) ...
安装完成之后,目前在Kali家目录,新建目录cmake
┌──(root㉿kidfu)-[~]
└─# mkdir cmake
┌──(root㉿kidfu)-[~]
└─# cd cmake/
在此目录之下新建main.cpp与CMakeLists.txt。注意名字不要写错了。并且粘贴以下内容
main.cpp
#include
int main()
{
std::cout << "cmake hello world";
return 0;
}
CMakeLists.txt,相信看名字就知道,第一行是定义cmake最低版本,第二行定义项目名称,第三行生成可执行二进制程序。
cmake_minimum_required (VERSION 3.10)
project(test)
add_executable(test main.cpp)
然后执行编译命令:
┌──(root㉿kidfu)-[~/cmake]
└─# mkdir build
┌──(root㉿kidfu)-[~/cmake]
└─# cd build
┌──(root㉿kidfu)-[~/cmake/build]
└─# cmake ..
-- The C compiler identification is GNU 13.2.0
-- The CXX compiler identification is GNU 13.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (8.3s)
-- Generating done (0.0s)
-- Build files have been written to: /root/cmake/build
查看当前目录内容,看看都生成了哪些程序。可以看到其实CMake也生成了Makefile文件。
┌──(root㉿kidfu)-[~/cmake/build]
└─# ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile
打开Makefile看看都有什么东西,这是一段很长的代码。所以可以想象CMake为我们忽略了多少底层的信息。
┌──(root㉿kidfu)-[~/cmake/build]
└─# cat Makefile
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.27
# Default target executed when no arguments are given to make.
default_target: all
.PHONY : default_target
# Allow only one "make -f Makefile2" at a time, but pass parallelism.
.NOTPARALLEL:
#=============================================================================
# Special targets provided by cmake.
# Disable implicit rules so canonical targets will work.
.SUFFIXES:
# Disable VCS-based implicit rules.
% : %,v
# Disable VCS-based implicit rules.
% : RCS/%
# Disable VCS-based implicit rules.
% : RCS/%,v
# Disable VCS-based implicit rules.
% : SCCS/s.%
# Disable VCS-based implicit rules.
% : s.%
.SUFFIXES: .hpux_make_needs_suffix_list
# Command-line flag to silence nested $(MAKE).
$(VERBOSE)MAKESILENT = -s
#Suppress display of executed commands.
$(VERBOSE).SILENT:
# A target that is always out of date.
cmake_force:
.PHONY : cmake_force
#=============================================================================
# Set environment variables for the build.
# The shell in which to execute make rules.
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake
# The command to remove a file.
RM = /usr/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /root/cmake
# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /root/cmake/build
#=============================================================================
# Targets provided globally by CMake.
# Special rule for the target edit_cache
edit_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "No interactive CMake dialog available..."
/usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available.
.PHONY : edit_cache
# Special rule for the target edit_cache
edit_cache/fast: edit_cache
.PHONY : edit_cache/fast
# Special rule for the target rebuild_cache
rebuild_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..."
/usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : rebuild_cache
# Special rule for the target rebuild_cache
rebuild_cache/fast: rebuild_cache
.PHONY : rebuild_cache/fast
# The main all target
all: cmake_check_build_system
$(CMAKE_COMMAND) -E cmake_progress_start /root/cmake/build/CMakeFiles /root/cmake/build//CMakeFiles/progress.marks
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all
$(CMAKE_COMMAND) -E cmake_progress_start /root/cmake/build/CMakeFiles 0
.PHONY : all
# The main clean target
clean:
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean
.PHONY : clean
# The main clean target
clean/fast: clean
.PHONY : clean/fast
# Prepare targets for installation.
preinstall: all
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall
.PHONY : preinstall
# Prepare targets for installation.
preinstall/fast:
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall
.PHONY : preinstall/fast
# clear depends
depend:
$(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1
.PHONY : depend
#=============================================================================
# Target rules for targets named test
# Build rule for target.
test: cmake_check_build_system
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 test
.PHONY : test
# fast build rule for target.
test/fast:
$(MAKE) $(MAKESILENT) -f CMakeFiles/test.dir/build.make CMakeFiles/test.dir/build
.PHONY : test/fast
main.o: main.cpp.o
.PHONY : main.o
# target to build an object file
main.cpp.o:
$(MAKE) $(MAKESILENT) -f CMakeFiles/test.dir/build.make CMakeFiles/test.dir/main.cpp.o
.PHONY : main.cpp.o
main.i: main.cpp.i
.PHONY : main.i
# target to preprocess a source file
main.cpp.i:
$(MAKE) $(MAKESILENT) -f CMakeFiles/test.dir/build.make CMakeFiles/test.dir/main.cpp.i
.PHONY : main.cpp.i
main.s: main.cpp.s
.PHONY : main.s
# target to generate assembly for a file
main.cpp.s:
$(MAKE) $(MAKESILENT) -f CMakeFiles/test.dir/build.make CMakeFiles/test.dir/main.cpp.s
.PHONY : main.cpp.s
# Help Target
help:
@echo "The following are some of the valid targets for this Makefile:"
@echo "... all (the default if no target is provided)"
@echo "... clean"
@echo "... depend"
@echo "... edit_cache"
@echo "... rebuild_cache"
@echo "... test"
@echo "... main.o"
@echo "... main.i"
@echo "... main.s"
.PHONY : help
#=============================================================================
# Special targets to cleanup operation of make.
# Special rule to run CMake to check the build system integrity.
# No rule that depends on this can have commands that come from listfiles
# because they might be regenerated.
cmake_check_build_system:
$(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
.PHONY : cmake_check_build_system
然后编译可执行文件。可以看到其生成了二进制程序test,其实这里生成test的名字和目录都是可选的,CMake支持高度可定制的功能,目前我还没发现CMake无法完成的。
┌──(root㉿kidfu)-[~/cmake/build]
└─# make
[ 50%] Building CXX object CMakeFiles/test.dir/main.cpp.o
[100%] Linking CXX executable test
[100%] Built target test
┌──(root㉿kidfu)-[~/cmake/build]
└─# ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile test
┌──(root㉿kidfu)-[~/cmake/build]
└─# ./test
cmake hello world
今天这篇文章讲解了CMake相关的背景知识,并且run了一个基本的hello world。
接下来我会从官方手册和自己的开发经验,一点一点记录CMake学习的经验,由于CMake涉及到编译原理,链接等,要做到深入浅出讲解是非常麻烦的,所以我可能会慢慢更新,今天就先到这里。
准备以后更新,如何建立一个大型工程的编译目录,如何包含三方库文件等。
[1] Kitware Inc. - Delivering Innovation - Customized Software Solutions for Complex Scientific Challenges