CMake 3.24.0源码自举、交叉编译与可搬运打包实践

发布时间:2026/10/24 4:42:38
CMake 3.24.0源码自举、交叉编译与可搬运打包实践

简介:这是一份面向 C++ 开发者与跨平台构建学习者的 CMake 3.24.0 源码发行包,适合需要研读构建工具实现、定制构建流程或对比版本差异的初学者与进阶开发者。压缩包共 2000 个文件,约 9.91MB,内容以 txt、cmake、rst、c、h、cxx、cpp 等为主,涵盖核心源码、模块脚本、测试用例、文档说明及部分工程配置,能较完整地呈现项目自身的组织方式与构建逻辑。已有 427 人学习下载,说明其在构建工具源码研读与工程实践方面具有一定参考价值。读者可了解项目配置文件的编写思路、命令与模块实现、跨平台检测机制及测试集成方式,也可用于本地编译验证、二次开发或版本行为对照。目录层次较丰富,适合按源码、模块、测试和文档分区查阅,对深入掌握 CMake 与 C++ 工程构建、理解其设计取舍较有帮助。

1. 拿到 cmake-3.24.0.tar.gz 之后:先判断这次自举值不值得做

同事把一个cmake-3.24.0.tar.gz丢过来,说构建机上的 cmake 太老,某个工程的预设文件读不进去,要求把版本固定住。这种场景下别急着./bootstrap,先判断:源码包真正解决的是「版本精确可控、不依赖发行版仓库、能塞进离网机器」,代价是十几分钟编译时间和一堆可选依赖。它就是一个自带自举脚本的 CMake 源码树,只需要一个支持 C++11 的编译器,就能把自己造出来。适合三类人:要复现某条 CI 的精确版本、目标机器装不了包、以及给交叉编译环境配一套固定 C++ 工具链的人。如果只是想跑个 hello world,发行版里那个够用,不必折腾。

2. 拆开源码包:bootstrap 三阶段自举和它真正依赖的东西

先把包解开看清楚再动手。很多人失败的根因不在命令写错,而在于脑子里把bootstrap当成了./configure——它其实是一个「先造一个小 cmake,再用这个小 cmake 造正式 cmake」的引导器,理解这一点,后面看日志才不会抓瞎。

2.1 包里都有什么,哪些目录决定成败

解包之后顶层是这些内容,先扫一眼,别一上来就make:

路径作用编译期能否省
bootstrap自举入口脚本,shell 实现不能
Bootstrap.cmk/自举阶段的构建目录,日志就在里面生成物,失败时进去看
Source/cmake、ctest、cpack、cmake-gui 的 C++ 源码主体不能
Modules/安装后进share/cmake-3.24/Modules的全部.cmake模块不能
Utilities/内置的 curl、zlib、expat、libarchive、bzip2 等第三方源码只有全走--system-*才能省
Tests/CMake 自己的回归测试集可关,能省将近一半编译时间
Help/、Auxiliary/文档、shell 补全可忽略

Modules/这个目录值得单独强调一句:CMake 的很多「语言特性」其实是模块文件实现的,find_package、CheckCXXSourceCompiles、GNUInstallDirs全在里边。所以安装时bin/和share/必须成套出现,只拷一个cmake可执行文件到/usr/local/bin是典型的翻车姿势,后面章节还会撞上。

2.2 bootstrap 为什么能编译出 cmake:三段自举流程

./bootstrap干的事分三段,每段的报错性质完全不同,分清了排查效率会高很多:

  1. 用系统 C++ 编译器,把Source/里一个精简版目标cmake_bootstrap编出来。这一阶段只看编译器能力,不看 cmake 版本,但只要内置第三方库(curl、zlib、openssl)的头文件缺失,就死在这里。
  2. 用刚编出来的cmake_bootstrap去 configure 完整源码树,生成CMakeFiles/、Makefile这类构建文件。这一阶段出错通常是模块逻辑或路径问题。
  3. 用生成的 Makefile 编正式的 cmake。这时才真正吃到并行度、内存和磁盘的亏。

三段的过程日志落在Bootstrap.cmk/cmake_bootstrap.log,这是个黑匣子,屏幕上的报错往往只有一句「bootstrap 失败」,真正的编译错误在里面。血泪经验:bootstrap 失败的第一动作不是重跑,是tail -n 80 Bootstrap.cmk/cmake_bootstrap.log。

2.3 依赖清单:一个 C++ 编译器和它的可选件

必需项只有 C++ 编译器、make、一个能装下编译中间产物的目录。可选项决定你得到什么能力,建议编译前先跑一遍下面这段探测脚本,别让编译到第 8 分钟才发现缺头文件:

#!/usr/bin/env bash set -u # 1) 编译器能力探测:bootstrap 只关心能不能吃 -std=c++11 g++ --version | head -n1 printf 'int main(){return 0;}' | g++ -std=c++11 -x c++ - -o /tmp/cc_probe && echo "c++11 ok" # 2) 并行度来源 make --version | head -n1 nproc # 3) 内存估算:单个大 .cpp 峰值约 1.2~1.8GB,并行度别超过 内存GB/2 free -g | awk 'NR==2{print "可用内存(G):",$7}' df -h . | tail -n1 # 4) 可选依赖:这些头文件决定 bootstrap 会不会半路死掉 ls /usr/include/openssl/ssl.h >/dev/null 2>&1 || echo "缺 openssl 头文件,内置 curl 会编不过" ls /usr/include/curl/curl.h >/dev/null 2>&1 || echo "缺 libcurl 头文件,--system-curl 不可用" ls /usr/include/curses.h >/dev/null 2>&1 || echo "缺 curses 头文件,ccmake 不会被构建" ls /usr/include/zlib.h >/dev/null 2>&1 || echo "缺 zlib 头文件" ls /usr/include/archive.h >/dev/null 2>&1 || echo "缺 libarchive 头文件,CPack 打包会受限"

这段脚本没有任何副作用,-std=c++11 -x c++ -是让编译器从标准输入读源码做一次真编译探测,比看版本号靠谱得多。内存那行是给并行度定上限用的,nproc只告诉你核数,不告诉你内存够不够——在 4GB 内存的构建机上开-j16,大概率是把自己送进 OOM killer。

提示:包管理器装依赖时,只装带-dev/-devel后缀的开发包,运行时库装了没用,bootstrap 要的是头文件。

3. 从解包到 make install:一份能照抄的编译脚本

整个流程我一般拆成三段执行,每段都落日志,失败时不用猜。下面这套命令在常规 x86_64 构建机上是稳的,参数含义逐条说明,你按自己环境改前缀和并行度即可。

3.1 解包位置与 bootstrap 参数逐个说

源码包只能「在哪解包就在哪编译」,bootstrap 不接受外部构建目录,所以解包位置要选一个磁盘够、后面还要留着跑测试的地方:

tar -xzf cmake-3.24.0.tar.gz cd cmake-3.24.0 # --prefix:安装前缀,带版本号方便多版本共存 # --parallel:自举阶段第三段的并行度,别写 nproc 死值,先按内存估 # --system-curl:用系统的 libcurl 替代内置 curl,绕开内置 curl 对 openssl 头的强依赖 # -- :后面透传给内部 cmake 的参数 ./bootstrap \ --prefix=/opt/cmake-3.24.0 \ --parallel=8 \ --system-curl \ -- -DBUILD_TESTING=ON 2>&1 | tee /tmp/bootstrap.log

参数里最值得琢磨的是--system-curl。默认情况下内置 curl 会连带内置 openssl 编译,一旦缺openssl/ssl.h就直接失败;换成系统 libcurl 后依赖变成curl/curl.h,多数构建机反而更干净。代价是系统 curl 版本偏老时,CMake 的下载相关功能(file(DOWNLOAD)、ExternalProject的URL拉取)会受限于系统 curl 的 TLS 能力。如果目标机器完全离网、只用本地find_package,这个代价可以忽略。

--system-zlib、--system-expat、--system-bzip2、--system-liblzma、--system-libarchive同理,都是「用系统库换编译时间与包体体积」。判断标准很简单:构建机上这套库版本统一、后续不会升级,就用系统的;要打成 tar 包发给一堆不确定的机器,就让 CMake 自带,省得运行时找不到.so。

再补一个容易被忽略的开关:-DBUILD_TESTING=OFF能砍掉测试集编译,时间少一半左右。但当你想验证这套 cmake 在目标机上行为正确时,测试集是唯一的后悔药,所以第一次自举建议留着ON,等验证通过再考虑关。

3.2 make 并行度、内存与安装前缀

bootstrap 成功后会生成顶层 Makefile,后面就是常规编译:

# 并行度取 min(核数, 内存GB/2),超了就等着看 Killed JOBS=$(( $(nproc) < $(free -g | awk 'NR==2{print int($7/2)}') ? $(nproc) : $(free -g | awk 'NR==2{print int($7/2)}') )) [ "$JOBS" -lt 1 ] && JOBS=1 echo "jobs=$JOBS" make -j"$JOBS" 2>&1 | tee /tmp/cmake-build.log # 安装到带版本号的固定前缀,不要直接进 /usr/local make install 2>&1 | tee /tmp/cmake-install.log

JOBS那行看着绕,实际就是把「核数」和「内存的一半」取小值,避免在内存吃紧的机器上把并行度开爆。我见过最典型的翻车是构建机 8 核 8GB,直接make -j8,编到Source/cmExecuteProcess附近被内核干掉,日志里只有一行Killed,看着像玄学,其实就是 OOM。

make install装到/opt/cmake-3.24.0之后,目录结构是这样的:bin/cmake、bin/ctest、bin/cpack、share/cmake-3.24/Modules、share/cmake-3.24/Templates、doc/、man/。share/cmake-3.24这个带版本号的目录名很关键,它保证了 3.24 和系统里的 3.22 不会互相覆盖模块文件——所以也别手贱把两个版本的share/合并。

启用方式建议走 PATH 而不是覆盖系统二进制:

# 只在当前 shell 生效,写进 CI 的 env 更可控 export PATH=/opt/cmake-3.24.0/bin:$PATH hash -r # bash 会把 cmake 的路径缓存住,不刷新会一直跑旧的那个 cmake --version

hash -r这一步在新机器上几乎每次都要,尤其你在同一个 shell 里先后用过系统 cmake 和新装的 cmake——which cmake显示新路径、cmake --version还是旧版本,就是这个缓存导致的。

3.3 安装后验证:版本、模块目录与 ctest 抽测

装完别只敲一句--version就收工,那只证明二进制能启动。至少要验三层:

# 第一层:版本与构建信息 /opt/cmake-3.24.0/bin/cmake --version # 第二层:能力清单与模块根目录,JSON 里能看出生成器、文件 API 等能力是否齐全 /opt/cmake-3.24.0/bin/cmake -E capabilities | head -c 400; echo # 第三层:模块目录是否成套安装(只拷 bin 的包在这里会露馅) ls /opt/cmake-3.24.0/share/cmake-3.24/Modules | head -n 5 test -f /opt/cmake-3.24.0/share/cmake-3.24/Modules/CMakeDetermineSystem.cmake && echo "modules ok" # 第四层:回归抽测,先看有没有注册上测试 cd /path/to/cmake-3.24.0 ctest -N 2>/dev/null | tail -n 3 ctest -R "RunCMake.CommandLine" --output-on-failure -j4

cmake -E capabilities输出的是 JSON,里面能看到支持的生成器列表和文件 API 版本,比版本号更能说明这套二进制是不是完整构建出来的。ctest -N是列举而不执行,如果这里显示 0 个测试,说明测试集在编译时被关了(BUILD_TESTING=OFF),后面的-R抽测也就无从谈起。

RunCMake.CommandLine这一类测试覆盖命令行解析与常见开关,跑通基本能确认这套 cmake 在自己的构建机上行为正常。抽测别贪多,全量跑要很久,挑几个和自己用法相关的就够了——常用生成器、find_package、try_compile各来一个。

4. 把这套 cmake 接进工程:工具链文件、Presets 与依赖注入

自举只是把工具造出来,真正省钱的地方在怎么把它接进工程。3.24 这个版本对「参数固化」和「依赖接管」的支持已经够用,配好之后构建命令能短到一行。

4.1 交叉编译工具链:C++ 编译器别塞进 CMAKE_CXX_FLAGS

交叉编译最坏的习惯是把一堆-I、-L、--sysroot塞进CMAKE_CXX_FLAGS,短期能编过,长期没人看得懂。正确姿势是一个独立的 toolchain 文件,由CMAKE_TOOLCHAIN_FILE引入:

# cmake/toolchain-arm64.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) # 编译器必须用绝对路径,避免 configure 阶段被 PATH 里的宿主 g++ 抢走 set(CMAKE_C_COMPILER /opt/toolchain/bin/aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER /opt/toolchain/bin/aarch64-linux-gnu-g++) set(CMAKE_SYSROOT /opt/sysroot/aarch64) # 查找规则:可执行程序用宿主的,库/头文件/包配置只许在 sysroot 里找 set(CMAKE_FIND_ROOT_PATH /opt/sysroot/aarch64) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # 目标程序在宿主上跑不起来,try_compile 的默认可执行探测会失败,改成静态库探测 set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

逐条说下为什么这么写。CMAKE_SYSTEM_NAME一设,CMake 就知道自己在交叉编译,会关掉一批「编译并运行」的探测;CMAKE_CXX_COMPILER用绝对路径是因为 configure 阶段会缓存编译器路径,用相对名很容易被宿主环境串味;CMAKE_FIND_ROOT_PATH_MODE_*那四行是最容易被忽略也最容易出事的——只设CMAKE_SYSROOT不设这四个 MODE,find_package依然可能摸到宿主机的/usr/lib,链接阶段才发现混进了 x86_64 的.so。最后那条CMAKE_TRY_COMPILE_TARGET_TYPE是交叉编译的救命开关,不设的话,任何check_cxx_source_compiles都会因为「编出来的程序没法执行」而返回失败,配置阶段直接红一片。

4.2 CMakePresets.json 固化参数与 --fresh 的真实用途

工具链文件写好后,把常用参数固化成预设,构建命令从七八行长龙变成两行:

{ "version": 4, "configurePresets": [ { "name": "arm64-release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/arm64-release", "toolchainFile": "${sourceDir}/cmake/toolchain-arm64.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } } ], "buildPresets": [ { "name": "arm64-release", "configurePreset": "arm64-release", "jobs": 8 } ] }

version字段是硬约束,写高了老 cmake 会直接拒绝解析并提示不支持的预设版本。稳妥做法是跟着你手上这套 cmake 的上限走,拿不准就写 4,功能足够覆盖工具链、缓存变量和构建并行度。toolchainFile是相对sourceDir解析的写法,比在cacheVariables里手写CMAKE_TOOLCHAIN_FILE更清楚;binaryDir带上预设名,多个预设互不干扰;jobs放在 buildPreset 里,避免每次手动敲--parallel。

对应的三条命令:

cmake --preset arm64-release # 配置 cmake --build --preset arm64-release # 构建 cmake --preset arm64-release --fresh # 丢掉缓存重新配置

--fresh是 3.24 里非常实用的一条。它会在配置前清掉CMakeCache.txt和CMakeFiles/,等价于以前手写rm -rf build/*再重来。为什么需要它?因为CMAKE_TOOLCHAIN_FILE是缓存变量,你改了 toolchain 文件内容后重新 configure,CMake 并不会重新读取它——它只在缓存为空时生效。表现就是「明明改了 sysroot 路径,编出来还是找不到新库」,折腾半小时才发现缓存没清。这条也是我踩过的坑。

4.3 用顶层注入接管 find_package

工程依赖一多,find_package找不到包是最常见的失败。3.24 提供了一个更干净的入口CMAKE_PROJECT_TOP_LEVEL_INCLUDES,它会在顶层project()调用时被include,时机早于工程里任何一行find_package:

# cmake/deps-provider.cmake # 这个文件在顶层 project() 时被加载,适合放全局查找策略 set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) # 把某个依赖固定到本地预编译产物,避免命中宿主机上版本不一致的那份 set(MyLib_DIR "/opt/deps/mylib/lib/cmake/MyLib" CACHE PATH "本地 MyLib 包配置路径") # 想接更复杂的依赖提供者机制(例如由外部包管理器接管),在这里注册即可 # cmake_language(SET_DEPENDENCY_PROVIDER my_provider SUPPORTED_METHODS FIND_PACKAGE)

调用方式:

cmake -S . -B build \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="$PWD/cmake/deps-provider.cmake"

这个变量只在顶层project()时生效,子项目里的project()不会触发它,project(NONE)也不触发;它接受的是文件列表,多个文件按顺序 include。实践中的边界很清楚:往里放「查找策略」和「变量重定向」这类轻量动作,别放编译逻辑或者耗时操作,因为它会在每次配置时执行,且执行时机非常早,那时工具链刚判定完,很多东西还没初始化。

注意:这个文件里不要写enable_language,也不要依赖任何由子目录设置的目标,配置阶段会直接报错。

5. 避坑与排查:自举失败的五个高频现场

下面五条都是我在构建机上真实撞过的,按「现象 → 原因 → 解决」写,遇到时对照着看,比从头读日志快。

5.1 bootstrap 报 Could not find OpenSSL

现象:./bootstrap跑几十秒后退出,屏幕最后几行提示找不到 OpenSSL 或 curl 相关目标,但机器上明明装了 openssl 命令行工具。

原因:内置 curl 需要的是 OpenSSL 的开发头文件和静态库,命令行工具属于运行时包,装了不等于能编译;容器镜像里尤其常见。

解决:三选一,优先第一条——装libssl-dev(或对应发行版的openssl-devel)后再跑;或者加--system-curl换成系统 libcurl,前提是有curl/curl.h;实在不行追加-- -DCMAKE_USE_OPENSSL=OFF关掉 TLS,代价是file(DOWNLOAD)走不了 https。改完记得把源码目录里的Bootstrap.cmk/清掉再重来,残留的探测结果会被复用。

5.2 make 到一半 Killed,日志里没有编译错误

现象:make -j16跑到某个大.cpp时进程消失,/tmp/cmake-build.log里只有一行Killed,没有任何 error。

原因:单个编译单元峰值内存一两 GB,并行度按核数开而不看内存,直接被内核 OOM killer 干掉。构建机跑着别的服务时更容易触发。

解决:把并行度按内存估,-j取内存GB/2与核数的较小值;临时救急可以用make -j4单点续编——make 会跳过已完成的目标,不用从头再来。长期方案是在构建机上给编译加 swap 或者干脆换台内存大的机器,别指望-O0能省多少。

5.3 明明装好了,configure 还在用旧版本

现象:which cmake指向/opt/cmake-3.24.0/bin/cmake,但cmake --version输出还是系统那个 3.22;或者新版本命令能跑,工程行为却是旧的。

原因:两种可能。bash 缓存了命令路径,PATH 改了不生效;或者工程build/CMakeCache.txt里缓存了CMAKE_COMMAND,重新 configure 时沿用了旧路径。

解决:先hash -r刷新命令缓存并重新cmake --version确认;确认无误后还不对,就cmake --preset <name> --fresh或者直接删掉 build 目录重配。多版本混用是这套流程最容易留下的技术债,建议在 CI 里显式打印cmake --version和cmake -E capabilities的哈希,出问题一眼定位。

5.4 交叉编译配置阶段红一片,check_cxx_source_compiles 全失败

现象:toolchain 配好后 configure 阶段大量探测失败,日志里反复出现「试编译的可执行文件无法运行」。

原因:try_compile默认编一个可执行文件并尝试运行,交叉编译环境下目标程序在宿主上根本跑不起来,所以所有「编译并运行」型探测一律判负。

解决:在 toolchain 文件里设CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY,让探测改成「只编译成静态库」,不运行。同时检查CMAKE_FIND_ROOT_PATH_MODE_LIBRARY是否设成ONLY,否则find_package可能摸到宿主库,链接阶段才报架构不匹配。

5.5 ctest 抽测一个都没跑

现象:ctest -R "RunCMake.CommandLine"输出「No tests were found」,但编译过程看起来一切正常。

原因:编译时关了BUILD_TESTING,测试目标压根没生成;或者在错误的目录下执行ctest,测试注册信息在构建树里,不在源码根上乱找。

解决:先ctest -N看注册数量,为 0 就说明测试没编;回到源码目录,确认当初 bootstrap 时-DBUILD_TESTING=ON并且make真的把测试目标编完了。想省时间又想要一点保障,可以只编部分测试目标,但别在关掉测试之后再抱怨「没法验证这套 cmake 是不是好的」。

6. 做成可搬运的工具目录:校验、瘦身与版本锁定

编译一次不容易,装完就该顺手做成交付物。CMake 本身是可搬迁的——它靠可执行文件所在位置推导自己的模块根目录,所以打成一个目录整体搬走也能用。我一般这么干:

#!/usr/bin/env bash set -euo pipefail SRC=cmake-3.24.0 PREFIX=/opt/cmake-3.24.0 STAGE=$(mktemp -d) # DESTDIR 方式安装到临时根,不动真实系统 make -C "$SRC" DESTDIR="$STAGE" install # 去掉调试符号,整个目录能小掉将近一半 find "$STAGE$PREFIX" -type f -perm -u+x -exec strip --strip-unneeded {} + 2>/dev/null || true mkdir -p dist tar -C "$STAGE$PREFIX" -czf "dist/cmake-3.24.0-$(uname -m).tar.gz" .

DESTDIR是安装时的临时根,配合--prefix能做到「装到临时目录、打包、再分发」,全程不污染构建机。strip那行只对可执行文件生效,失败也不影响打包,所以加了|| true。打出来的包解压即用,但必须验证一次搬迁后还能跑,否则发出去才发现模块路径硬编码,那就难看了:

T=$(mktemp -d) tar -C "$T" -xzf "dist/cmake-3.24.0-$(uname -m).tar.gz" cat > "$T/smoke.cmake" <<'EOF' message(STATUS "cmake: ${CMAKE_VERSION}") message(STATUS "root : ${CMAKE_ROOT}") if(NOT EXISTS "${CMAKE_ROOT}/Modules/CMakeDetermineSystem.cmake") message(FATAL_ERROR "模块目录缺失,这个包不可搬运") endif() execute_process(COMMAND "${CMAKE_COMMAND}" --version OUTPUT_VARIABLE v) message(STATUS "self : ${v}") EOF "$T/bin/cmake" -P "$T/smoke.cmake"

这个 smoke 脚本查三件事:CMAKE_ROOT是否被正确推导到解压目录、Modules/是否完整、以及CMAKE_COMMAND自引用能不能跑通。三段全绿,这个 tar 包才算能交付。之前吃过一次亏:某次为了省体积只挑了bin/和部分模块,结果在离网机器上find_package(Threads)直接失效,报的错还特别绕,说找不到FindThreads.cmake——查了半天才发现是自己裁剪掉的。

顺手再锁一层版本信息,让这套工具能自证身份:

cat > dist/BUILD-INFO.txt <<EOF package : cmake-3.24.0 arch : $(uname -m) built : $(date -u +%Y-%m-%dT%H:%M:%SZ) builder : $(g++ --version | head -n1) options : --prefix=/opt/cmake-3.24.0 --parallel=8 --system-curl sha256 : $(sha256sum dist/cmake-3.24.0-$(uname -m).tar.gz | cut -d' ' -f1) EOF

把 bootstrap 参数原样记进BUILD-INFO.txt是一件回报很高的小事:半年后有人问「这套 cmake 是怎么编的、带不带内置 curl」,翻一眼文件就有答案,不用去翻构建机历史。校验和也顺手算了,分发时对齐一下,能避免传输损坏这类低级事故。

从那以后我每次交付构建机镜像,流程都固定成「自举 →ctest抽测 →DESTDIR打包 → 搬迁后跑 smoke → 写 BUILD-INFO」,少一步都不敢打 tag。这套习惯救过我不止一次,尤其是「改了 toolchain 文件却忘了--fresh」和「只拷了 bin 忘了 share」这两个反复出现的坑。希望帮到你。

本文还有配套的精品资源,点击获取