先交代一个前提:这篇文章里所有操作我都用“源码目录”指代LLVM的检出目录,“构建目录”指代CMake生成的编译目录,避免路径差异干扰。我实测过Windows 10/11两代的x64环境,VS 2019和2022都跑过,MinGW用的也是当前主流的w64线程模型版本。下面这些经验基本都来自实际编译时的记录,踩过的坑也会一并写清楚。
1. 为什么要在Windows上编译LLVM
网上关于LLVM的讨论很多,默认大家用的都是Linux。但实际工作中总有一些场景必须在Windows下编译,比如给某个跨平台项目做代码分析工具链、在公司内网没法直接拉官方release包、或者你想给LLVM本身打补丁调试。我自己第一次编译LLVM是在做某跨平台系统的性能分析模块时,需要用Clang的静态分析功能,但内网环境和目标平台都是Windows,只能硬着头皮在Windows上折腾工具链。
Windows下编译LLVM,本质上就是两条路:用Visual Studio的MSVC编译器,或者用MinGW-w64的GCC编译器。前者主要产出一套完整的.sln解决方案,方便在IDE里边改代码边编译;后者走的是Classic Makefile或Ninja风格,更像在Linux上操作。两条路都能编译出可用的llvm、clang、lld,但细节差异非常大,选错方向会浪费很多时间。
很多人问为什么不直接下载官方预编译包。答案是:能用官方包当然最省事,但如果你要改LLVM源码、调试pass、或者定制clang工具链,就必须自己编译。而且Windows上编译LLVM的门槛并没那么高,把CMake参数理清楚之后,成功率甚至比想象中高。这篇就是想把我在配置、编译、排错过程中沉淀下来的东西捋一遍,给准备动手的人一份可参照的路线。
1.1 什么人需要自己编译LLVM
需要自己动手的场景大致有三类。第一类是给LLVM上的子项目做定制,比如要给Clang加自定义warning、给后端target加指令支持,这些都必须从源码编译。第二类是性能敏感场景,官方release包的build type通常是Release,但如果你要调试编译过程本身,需要Debug或RelWithDebInfo版本。第三类则是公司内网的离线构建,既没有外网下载条件,又要保证工具链可控,源码编译就成为唯一选择。
这三种场景我都遇过。最痛苦的不是编译本身,而是Windows上CMake生成器和工具链之间的配合。VS生成器下,CMake会把平台信息交给MSBuild处理,不太需要操心环境变量;MinGW模式下,你要是把编译器路径配错了,或者用了不匹配的make程序,会在configure阶段就报各种莫名其妙的“Unable to find”错误。所以这篇文章会重点把这两种模式的配置差异讲清楚。
1.2 VS和MinGW两条路线怎么选
先说结论:如果你主要工作环境是Visual Studio,打算在IDE里开发、调试LLVM,选VS生成器,也就是用MSVC编译。这有个很大的优势:LLVM官方把MSVC作为Windows上的一等公民,相关构建脚本都维护得比较完整,clang-cl的配置也是默认考虑过的。如果你更习惯Linux那套命令行习惯,或者你编译出来的工具链要配合GCC风格使用,选MinGW更顺手。
但要注意一个关键点:MinGW路线下,编译器是gcc/g++,链接器是GNU ld,而CMake里的生成器可以是“MinGW Makefiles”,也可以是“Ninja”。很多人把生成器和编译器混淆。实际组合上,我推荐MinGW搭配Ninja,因为Ninja的并行度高,编译速度明显快过原来的mingw32-make,而且增量构建稳定很多。VS路线下则不需要Ninja,直接用MSBuild。
两条路线的资源消耗也不同。实测下来,全套LLVM+Clang+lld编译,VS模式生成的中间文件会多一截,整个构建目录轻松占掉25GB到30GB;MinGW+Release模式大概18GB左右。内存上,如果只开单项目比如LLVM核心,8GB内存够用;但一旦开了clang,建议至少16GB,否则容易在链接阶段被内存撑爆。
2. 动手前的准备:工具链和依赖清单
编译LLVM最怕的不是编译过程,而是环境不干净。这一步我吃过亏:一开始只装了VS没装“单个组件”里的C++工具集,CMake configure直接报找不到MSVC工具链,排查了很久才发现是组件缺失。所以动手前,先把环境列个清单逐项过一遍,能省很多后续折腾。
2.1 Visual Studio安装时容易被忽略的组件
安装VS时,大家通常只勾了“使用C++的桌面开发”这个工作负载。但对LLVM来说,有些头文件和库的依赖是隐含的。建议检查一下这几个选项:
- Windows 10/11 SDK:默认会装,但注意如果只装了旧版本,clang编译Windows头文件时可能出问题。
- MSVC v143或v142生成工具:这是核心编译器,不装等于白搭。
- C++ CMake tools for Windows:可以帮VS里的CMake项目省点事,但这里不是必须,因为我们用命令行cmake。
- Git:建议装上,后面拉源码用。
安装好之后,打开“开发者命令提示符”或者任何终端,输入cl命令能识别,就说明MSVC环境可用了。注意,普通终端是找不到cl的,必须用VS提供的“Developer PowerShell”或“x64 Native Tools命令提示符”进入。
2.2 MinGW-w64的选择与配置
MinGW有两个大版本:一个老牌的MinGW-w64 GCC系列,另一个是WinLibs构建版。我建议用WinLibs或者从合适渠道下载的w64版gcc,因为版本较新,还带了gdb等工具。务必选64位版本,LLVM全量编译用32位工具链你会非常难受。
装好后按gcc --version能输出版本号即可。之后要把MinGW的bin目录加到系统PATH里,比如C:\mingw64\bin。这里有个坑:如果PATH里同时存在多个gcc,CMake会选出第一个,可能导致后面生成器探测不一致。我踩过这个坑,所以建议把MinGW的bin路径放在PATH靠前位置,或者干脆在CMake配置时手动指定编译器路径。
2.3 CMake、Python、Ninja的版本要求
LLVM对CMake版本有最低要求,大体上是3.20以上。建议直接用最新的3.27或3.28,旧版本在某些新配置项上会直接warning甚至报错。Python必须装3.6以上,因为LLVM构建脚本里大量使用了Python做代码生成,我实测过Python 3.11、3.12都能正常工作。Ninja单独下载一个最新release就可以,把ninja.exe扔进PATH即可,无需安装。
这里顺带解释一下为什么LLVM需要Python:llvm-tablegen等工具是基于C++实现的,但一些测试脚本和lit测试框架是Python写的,另外clang的一些编译选项生成脚本也调用了Python。没有Python,configure阶段过了但构建阶段随时可能挂。
2.4 磁盘、内存和CPU资源评估
LLVM全量编译对资源的要求超过很多人预期。最低建议是16GB内存、30GB可用磁盘、4核以上CPU。我只编译LLVM核心和Clang时,Release构建耗时在8核机器上大约40到60分钟;如果全开所有target,时间会翻倍。
如果内存只有8GB,也有办法:控制并发数、关闭CCache、只编核心target。但这会牺牲很大效率。更实际的建议是:能让机器跑Linux虚拟机就不要在Windows上折腾,但如果项目硬要求在Windows平台,那至少把内存加到16GB。
3. CMake配置的完整拆解
这一步是整个编译过程的灵魂。LLVM的构建系统非常灵活,但也因为灵活,很多人容易被一堆-D参数吓住。其实只要理解两件事就行:生成器选谁、项目开哪些。剩下就是围绕这两个主决策去补充参数。
3.1 获取源码的正确姿势
源码获取我推荐用git克隆官方仓库的release分支,而不是直接下压缩包。因为LLVM后续往往会切tag、更新submodule,git管理起来方便很多。命令很简单:
git clone --depth=1 -b llvmorg-17.0.6 https://github.com/llvm/llvm-project.git注意--depth=1能省大量下载时间,但如果你打算长期调试历史版本,最好别加这个参数,而是完整克隆。LLVM仓库里包含了llvm、clang、lld、libcxx等所有官方子项目,都在同一个仓库内,不需要单独下载。
3.2 核心配置项逐个说明
CMake配置项很多,但真正影响成败的就这几个:
CMAKE_BUILD_TYPE:Release、Debug、RelWithDebInfo、MinSizeRel。如果只是要工具链用,选Release;要调试LLVM内部逻辑,选RelWithDebInfo或Debug。注意Debug版本体积巨大且慢,初学者慎选。LLVM_ENABLE_PROJECTS:你需要的子项目,用分号分隔。我一般写clang;lld,有需要再加clang-tools-extra。这个参数一定要在第一次configure就写对,因为LLVM要求在配置阶段就决定要构建哪个子项目,后面改可能引发全量重建。LLVM_TARGETS_TO_BUILD:默认会构建所有支持的后端target,比如X86、ARM、AArch64、RISCV等。很多人没意识到这是个巨大的时间黑洞。如果你只是在x86机器上做常规开发,写X86就足够,能省掉近一半编译时间。需要交叉编译再加对应后端。LLVM_ENABLE_ASSERTIONS:默认跟build type走,Release下是OFF。做开发调试建议显式打开,但要注意会降低运行时性能。CMAKE_INSTALL_PREFIX:最终安装路径,建议单独指定,比如D:\LLVM-17,这样ninja install出来干干净净。
还有几个值得注意的次要点:LLVM_USE_LINKER可指定使用lld作为链接器,能显著加快链接;LLVM_CCACHE_BUILD建议打开,多次增量构建会省时间;CLANG_ENABLE_STATIC_ANALYZER一般默认开,不消耗额外资源。
3.3 使用VS生成器的配置示例
VS模式的配置命令长这样。注意必须在“开发者PowerShell”或“x64 Native Tools命令提示符”里运行:
cmake -G "Visual Studio 17 2022" -A x64 -Thost=x64 ` -DCMAKE_BUILD_TYPE=Release ` -DLLVM_ENABLE_PROJECTS="clang;lld" ` -DLLVM_TARGETS_TO_BUILD="X86" ` -DLLVM_ENABLE_ASSERTIONS=OFF ` -DCMAKE_INSTALL_PREFIX="D:\LLVM-17" ` ../llvm-project/llvm-T host=x64表示宿主工具用64位,否则默认可能是32位,编译大项目容易撑爆进程。-A x64指定目标架构。配置完成后,CMake会在构建目录里生成一个LLVM.sln,之后用VS打开,选好配置和平台就能Build。当然也可以走命令行,用msbuild或者cmake --build。
VS模式最大的好处是IDE集成舒服,断点调试源码时,VS的Source Link和符号处理都很成熟。但坏处是,如果代码里改了CMakeLists,用VS重新配置时偶尔会出现缓存不刷新的情况,需要手动删除CMakeCache再重新配置。
3.4 使用MinGW生成器的配置示例
MinGW模式我推荐Ninja生成器,配置命令如下:
cmake -G "Ninja" ` -DCMAKE_BUILD_TYPE=Release ` -DCMAKE_C_COMPILER=gcc ` -DCMAKE_CXX_COMPILER=g++ ` -DLLVM_ENABLE_PROJECTS="clang;lld" ` -DLLVM_TARGETS_TO_BUILD="X86" ` -DLLVM_ENABLE_ASSERTIONS=OFF ` -DCMAKE_INSTALL_PREFIX="D:\LLVM-17-mingw" ` ../llvm-project/llvm这里-G "Ninja"指定生成器,CMAKE_C_COMPILER和CMAKE_CXX_COMPILER手动指定编译器路径,防止CMake用了环境变量里其它gcc。执行完cmake --build . -j 8就能开编。
有个地方要特别提醒:Ninja模式下不要混用cmake和系统make。很多人配好后忘了装ninja,就顺手把-G改成"MinGW Makefiles",再用mingw32-make去编。这也能成,但遇到增量构建时比较挑TMP环境变量,偶尔会出现resume时重新编译大量文件的情况。所以Ninja还是优先选择。
3.5 两种生成器的本质区别
这两种生成器的区别值得单独说清楚。VS生成器是多配置的,它会把编译、链接命令打包到.sln里,你可以在同一个构建目录里反复切换Debug/Release而不重新配置CMake。MinGW/Ninja生成器是单配置的,它在configure阶段就定死了build type,切换Release到Debug需要重新配置一个单独的目录。
也就是说,VS模式下你惯用的“改个选项就重新编译”在MinGW模式下行不通。我实际工作里会准备两个目录,一个build-release供日常用,一个build-debug供调试用,这样互不干扰。磁盘空间足够的话非常推荐这种双目录方案。
4. 构建实战与验证
配置完成之后,就是漫长的编译过程。我见过不少人在这一步因为并发太高或磁盘空间不足翻车,所以这部分把构建过程中的实操要点和验证方式一并讲透。
4.1 VS方式构建与增量编译
VS模式下,你可以直接在VS里按F5或Ctrl+B构建,也可以命令行执行。命令行下建议这样:
cmake --build . --config Release --parallel 8--parallel 8对应CPU核数。如果编译过程中你改了源码,再跑一次同样的命令,MSBuild只编译变更过的文件,这就是增量编译。不过要注意,如果你改了CMakeLists.txt或者llvm-config.h.cmake这类配置文件,MSBuild会触发重新生成CMake相关文件并重新编译一大批文件,这是正常现象。
VS模式下还有个隐藏坑:默认的字符集和编译选项与LLVM的脚本可能产生冲突。特别是某些老版本LLVM在MSVC下编译llvm-tblgen时会报C4715等warning,一般不影响结果,但建议在CMake里加-DLLVM_ENABLE_WARNINGS=OFF来减少噪音。
4.2 MinGW方式构建
MinGW+ Ninja的构建命令很干净:
cmake --build . --parallel 8ninja会自动读取构建目录里的配置。第一次编译LLVM+Clang时,看到大量编译日志滚动是正常的。建议把输出重定向到日志文件,比如:
cmake --build . --parallel 8 2>&1 | tee build.log万一中途报错,可以在日志里搜error:关键词定位问题,避免一遍遍翻终端。
Ninja模式下,链接阶段的内存压力比VS模式更明显。我遇到过一次ld进程直接崩溃,就是并发太高导致的。解决办法是降低并行度,或者给链接器指定-Wl,--threads参数减少内存占用。更稳妥的是在CMake配置时加一句-DLLVM_USE_LINKER=gold,用gold链接器比默认的bfd链接器占用内存小不少。
4.3 产物验证用法:检查clang和llc
编译完成或者中途,怎么验证产物可用?其实不用等装完,直接在构建目录里就能找到二进制。我一般先验证三个东西:
bin\clang.exe --version:确认clang能运行、版本号正确、target是x86_64-pc-windows-msvc或gnu。bin\llc.exe --version:注册的后端target列表里是否包含x86。bin\llvm-config.exe --components:看核心库和组件是否齐全。
写一个最简单的C文件,用编译出的clang编译成目标文件:
#include <stdio.h> int main() { printf("hello llvm\n"); return 0; }然后执行:
bin\clang.exe -c hello.c -o hello.o如果能正常生成hello.o,就说明编译器主干流程是通的。想更进一步的话,可以链接成可执行文件,MinGW模式下用它编译的exe体积会偏大,这是正常的,GCC的默认启动文件和静态链接风格决定了这一点。
5. 常见问题与排查技巧实录
编译LLVM最让人头疼的往往是问题五花八门,而且很多错误信息在网上搜不到准确答案。这一节我把几个真实撞过墙的问题整理出来,按概率排序。
5.1 路径与编码:空格、中文目录
这个问题几乎必躺。CMake配置阶段如果源码根目录或构建目录包含空格、中文或者特殊符号,Ninja和MSBuild都会出诡异错误。比如D:\Program Files\llvm-project,Ninja在构建时会报“ninja: error: expected '='”之类的信息,一开始根本想不到是路径问题。MSVC编译器也会对非ASCII路径产生异常行为。
解决方案非常简单:所有的目录都用纯英文路径,中间不要有空格。最稳的是建一个像D:\llvm-src、D:\llvm-build这样的短路径,从头到尾避开麻烦。另外Windows系统用户名如果是中文,默认的Temp路径可能也带着中文,这时候先手动设置环境变量TMP和TEMP到英文目录,再启动编译,能避免很多奇奇怪怪的报错。
5.2 VS编译内存耗尽
用VS构建时候,最经典的是链接阶段报LNK1102: invalid object file或者fatal error LNK1102: 无法打开文件,这种多半是内存不足。MSVC的link.exe在链接clang和lld这种大型可执行文件时,峰值内存可以到4GB甚至更高。并行链接时如果开了8路,内存很容易冲破16GB。
遇到LNK1102,先不要怀疑代码问题。我的解决办法是降低/maxcpucount,或者在VS里一次只构建一个目标,不要选择“Build Solution”而是单独Build clang这个target。还有一个更巧的办法,先用build-bin顺序编几个小工具,把磁盘缓存热起来,再编大目标,内存压力会小一些。
5.3 MinGW链接错误
MinGW模式下的经典错误是undefined reference to_imp...``,一般出现在链接阶段调用Windows系统库的时候。这通常是编译器路径配置有问题,导致gcc链接时没有找到正确的libkernel32.a等导入库。排除方法很简单:执行gcc -v看下它的library search路径是否指向你的mingw64目录。
另一个常见错误是direct2d相关的Undefined symbols,这通常是因为打了老版本SDK头文件。解决办法是确认Windows SDK版本,并让clang或gcc附带-D_WIN32_WINNT=0x0A00这类宏定义,保证头文件识别对系统版本。
5.4 构建太慢怎么办
构建慢不是错误,但比错误更折磨人。想提速,第一件事是用ccache或者sccache。Windows上sccache配合MSVC非常好用,第二次全量构建能减少一半时间。我配置过一次sccache,重编LLVM+Clang时编译时间从45分钟压缩到20分钟左右,效果显著。
其次检查是不是把不必要的东西编进来了。如果LLVM_TARGETS_TO_BUILD是ALL,那你正在为几十种架构浪费时间。我见过一个同事为了写一个x86后端pass,把ARM、MIPS、RISCV全编了,磁盘多占8GB,时间多花30分钟,完全没有必要。
最后,如果你的机器有多块盘,把源码放在SSD上,把构建目录放在另一块SSD或机械盘上,实际体验差别能感觉得出来。构建目录的随机读写量非常大,机械盘会让编译时间显著拉长。
6. 编译之后还能做点什么
很多人以为编译完就结束了,其实真正的价值在产物怎么用。我编译LLVM不只是为了跑clang,更多是为了两个目的:一是给某个图像处理Demo做自动向量化实验,二是基于LLVM写一个简单的自定义Pass做IR变换。
拿到自编译的clang后,除了日常编译代码,我最推荐做的事是跑一遍llvm-lit -sv test/里的简单测试集。编译出来的构建目录中自带lit和测试配置,直接在构建目录里运行ninja check-llvm,能跑几千个回归测试。这不仅能验证工具链正确性,也是熟悉LLVM测试体系的第一步。
对于想做二次开发的人,建议先读一下llvm/lib/Passes/PassBuilder.cpp和llvm/include/llvm/IR/PassManager.h,这是LLVM新的Pass管理框架核心,也是后续写Pass必用的入口。Windows下调试时,VS的调试器能正常识别LLVM的源代码符号,比在很多IDE里方便得多。MinGW模式下想要调试,可以生成DWARF调试信息,用gdb和VSCode配合也能跑,但体验上确实比VS差一点。
我个人在实际操作里的体会是,Windows环境编译LLVM真正的门槛不在工具链本身,而在“CMake生成器和工具链的匹配关系”以及“会不会正确控制构建范围”这两件事上。这些参数在官方文档里都有,但没人告诉你组合顺序和坑。把环境理顺之后,LLVM的构建流程其实非常机械,完全可以自动化脚本化,后面再编译别的版本就只是改几个版本号的问题。最后再分享一个小技巧:把从configure到build的整条命令存成批处理脚本,每次改动只用改版本号和目标名,这样即使半年后再来一次,也不会因为陌生而手忙脚乱。