笔者在使用 `rbenv`[1] 安装 ruby 时,遇到一个头文件缺失导致无法编译失败的问题。
本文会记录笔者对该问题产生的原因分析,并通过分析 clang 源码的方式提供一个通用的解决方案。
通过该方案,可以解决所有类似的头文件缺失问题。
rbenv 安装 ruby 失败
笔者是执行 rbenv install 2.7.2 命令时遇到了问题。
控制台输出如下:

image-20201214210046129
根据添加 --verbose 参数,我们可以得到更加详细的错误信息:
clang -I. -Iinclude -fPIC -arch x86_64 -O3 -Wall -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAESNI_ASM -DVPAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -DOPENSSLDIR=""/Users/kukudeaidian/.rbenv/versions/2.7.2/openssl/ssl"" -DENGINESDIR=""/Users/kukudeaidian/.rbenv/versions/2.7.2/openssl/lib/engines-1.1"" -D_REENTRANT -DZLIB -DZLIB_SHARED -DNDEBUG -I/Users/kukudeaidian/.rbenv/versions/2.7.2/include -MMD -MF apps/app_rand.d.tmp -MT apps/app_rand.o -c -o apps/app_rand.o apps/app_rand.c
In file included from apps/app_rand.c:10:
In file included from apps/apps.h:13:
In file included from ./e_os.h:16:
In file included from include/openssl/e_os2.h:243:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/12.0.0/include/inttypes.h:21:15: fatal error: 'inttypes.h' file not found
#include_next <inttypes.h>
^~~~~~~~~~~~
1 error generated.
make[1]: *** [apps/app_rand.o] Error 1
make: *** [all] Error 2
从上面的日志中,我们可以发现三个关键点:
rbenv最终调用了clang执行编译任务clang执行编译任务时,无法找到系统库头文件<inttypes.h>clang命令缺失-isysroot参数
系统库文件查找路径
通常情况下,我们可以添加参数 -isysroot <dir> 的方式解决上面的报错:
clang -c -o apps/app_rand.o apps/app_rand.c -I. -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
但是,本次是通过 rbenv 命令执行 ruby 的安装任务,所以上面的解决方案行不通。
Clang driver
为了查找变通方案,我们需要先了解一下 `Clang driver`[2] 。
在 llvm 编译器高级用法:第三方库插桩中,我们曾经提到过 clang 会按照以下顺序执行。

image-20201215013642049
而负责组装每个任务的就是 clang Driver。
clang Driver 的处理逻辑分为以下几步:
- Parse: Option Parsing:解析传入的参数
- Pipeline: Compilation Action Construction:根据每个输入的文件和类型,组建
action(比如PreprocessJobAction)- 通过
-ccc-print-phases可以查看action
- 通过
clang -ccc-print-phases -c t0.c t1.c
- 0: input, "t0.c", c
- 1: preprocessor, {0}, cpp-output
- 2: compiler, {1}, ir
- 3: backend, {2}, assembler
- 4: assembler, {3}, object
5: bind-arch, "x86_64", {4}, object
- 6: input, "t1.c", c
- 7: preprocessor, {6}, cpp-output
- 8: compiler, {7}, ir
- 9: backend, {8}, assembler
- 10: assembler, {9}, object
11: bind-arch, "x86_64", {10}, object
Bind: Tool & Filename Selection:根据 action 选择对应的工具和文件名信息
- 通过
-ccc-print-bindings可以查看对应的工具和文件名信息clang -ccc-print-bindings -c t0.c t1.c -arch arm64 -arch armv7# "aarch64-apple-darwin19.6.0" - "clang", inputs: ["t0.c"], output: "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t0-9a2aac.o"# "arm-apple-darwin19.6.0" - "clang", inputs: ["t0.c"], output: "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t0-7a4059.o"# "arm-apple-darwin19.6.0" - "darwin::Lipo", inputs: ["/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t0-9a2aac.o", "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t0-7a4059.o"], output: "t0.o"# "aarch64-apple-darwin19.6.0" - "clang", inputs: ["t1.c"], output: "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t1-950abb.o"# "arm-apple-darwin19.6.0" - "clang", inputs: ["t1.c"], output: "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t1-044386.o"# "arm-apple-darwin19.6.0" - "darwin::Lipo", inputs: ["/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t1-950abb.o", "/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/t1-044386.o"], output: "t1.o"
Translate: Tool Specific Argument Translation:根据输入的参数转为不同tool 的参数
原始参数:
代码语言:javascript复制 xcrun --sdk iphoneos clang -target arm64-apple-ios8.0 main.m -v
各个 tool 的参数:
clang -cc1 -triple arm64-apple-ios8.0.0 -o main-a28fc8.o -x objective-c main.m
ld -arch arm64 -platform_version ios 8.0.0 -o a.out main-a28fc8.o
- 比如,插桩参数
-fsanitize-coverage=trace-pc-guard会变为-fsanitize-coverage-type=3 -fsanitize-coverage-trace-pc-guard参数
Execute:真正的执行不同的工具
为了方便理解,我们可以将下面的图片和上面的流程对应:

DriverArchitecture
第一版方案:通过环境变量控制头文件搜索路径
因为 mac 与 Darwin tool chain 对应,所以我们需要重点关注 Darwin tool chain 相关的逻辑。
通过查看 clang::driver::toolchains::Darwin 相关的代码,我们会发现下面的执行逻辑:
Compilation 调用 clang::driver::toolchains::Darwin 的 TranslateArgs函数时,会在命令行的 -isysroot 参数缺失的情况下,通过环境变量 SDKROOT 获取。

image-20201214222425629

image-20201215013626760
如下图,通过 export 命令设置环境变量后,clang 命令可以正常执行
export SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

image-20201215010258221
xcrun
考虑到不同的系统会对应不同的 SYSROOT,每次都通过 export 方式设置环境变量比较繁琐,我们需要换一种更简单的配置方式。
根据 xcrun 的帮助文件,我们可以发现,通过 xcrun 命令可以方便的调整不同系统对应的 SDKROOT 路径。
SDKROOT
Specifies the default SDK to be used when looking up tools (some tools may have SDK specific versions).
This environment variable is also set by xcrun to be the absolute path to the user provided SDK (either via SDKROOT or the
--sdk option), when it is used to invoke a normal developer tool (build tools like xcodebuild or make are exempt from this
behavior).
For example, if xcrun is used to invoke clang via:
xcrun --sdk macosx clang test.c
then xcrun will provide the full path to the macosx SDK in the environment variable SDKROOT. That in turn will be used by
clang(1) to automatically select that SDK when compiling the test.c file.
比如,我们可以通过以下代码完成编译:
代码语言:javascript复制// 编译 iPhone 项目
xcrun -l --sdk iphoneos clang -target arm64-apple-ios8.0 main.m
// 编译 macosx 项目
xcrun -l clang main.m
优化方案:xcrun
根据上面的信息,我们可以尝试使用 xcrun 调用安装命令:

image-20201215012636981
通过截图,我们可以发现 xcrun rbenv install 2.7.2 命令组合可以安装 ruby。
总结
通过本文,我们可以得到以下经验:当因为标准库头文件缺失导致编译失败时,可以通过搭配 xcrun 完成编译任务。
参考资料
[1]
rbenv: https://github.com/rbenv/rbenv
[2]
Clang driver: https://clang.llvm.org/docs/DriverInternals.html


