
老项目必须锁死_MSC_VER、客户只认某套MSVC编译器的ABI行为、或者CI机不能提前装好几个版本的Visual Studio——遇到这类场景直接在vcxproj里改PlatformToolsetv143/PlatformToolset是解决不了的因为v143工具集的路径是跟当前VS实例绑定的。我前阵子在一个遗留C工程上就撞上了这个问题代码是VS2017时代写的里面大量依赖老编译器的容错行为拿到VS2022上编译直接变成WAVE般的警告和错误。折腾一圈之后我决定基于现有v143的PlatformToolset目录结构造一个自定义工具集把调用目标指向老版MSVC编译器。这个方法走通后效果很稳定而且不污染系统自带的v143文件。这篇文章就把我从拆解Toolset.props/.targets到写出可复用自定义工具集的完整过程写出来。适合以下读者想在新版VS里使用老编译器、正在做本地/CI工具链统一、或者只是想把PlatformToolset这个下拉框彻底搞明白的人。1. 先搞清楚新版VS里的v143工具集究竟是靠什么定位编译器的很多人的第一反应是直接改v143的Toolset文件把路径指到老版本。我不建议这么干原因后面会讲。但在动手之前你至少要明白v143工具集的目录结构和发现机制否则自定义工具集会做成“能用但不知道为什么能用”。1.1 工具集文件藏在哪个目录MSBuild怎么发现它VS2022的MSBuild相关文件默认在安装目录下C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\注意这里的关键目录是v170对应VS2022的MSBuild工具集版本号。在这下面你可以看到完整的Platforms目录Platforms\Win32\PlatformToolsets\v143\ Platforms\x64\PlatformToolsets\v143\ Platforms\ARM\PlatformToolsets\v143\ Platforms\ARM64\PlatformToolsets\v143\每个平台目录下的v143文件夹里核心文件就是Toolset.props和Toolset.targets。有的版本还会有其他辅助文件但真正决定“工具集是什么、编译器在哪、版本号是多少”的就是这两个。MSBuild的发现逻辑是这样的当vcxproj里写了PlatformToolsetv143/PlatformToolsetMicrosoft.Cpp.Default.props会按当前Platform去拼路径在PlatformToolsets\$(PlatformToolset)目录下找Toolset.props并导入。找不到目录的话就会输出那句经典的报错error MSB8020: The build tools for v142 (Platform Toolset v142) cannot be found.这个机制很关键——它说明MSBuild判断工具集是否存在的唯一依据就是文件目录是否存在而不是注册表、不是安装器记录。只要你在对应平台目录下建一个文件夹放一个格式正确的Toolset.propsMSBuild就会老老实实地按你的规则走。1.2 v143的Toolset.props和.targets里到底写了什么以一台装了VS2022 17.6左右的机器为例打开v143\Toolset.props核心内容大致是Project PropertyGroup PlatformToolsetv143/PlatformToolset VCToolsVersion14.36.32532/VCToolsVersion MSBuildWSDKNumber10.0.19041.0/MSBuildWSDKNumber /PropertyGroup /Project不同补丁版本里VCToolsVersion的数值不同但作用都是一样的声明“这个工具集默认使用哪个小版本的MSVC工具链”。再打开Toolset.targets内容更短核心就是把VCToolsInstallDir指到具体工具链目录Project PropertyGroup VCToolsInstallDir$(VCInstallDir)Tools\MSVC\$(VCToolsVersion)\/VCToolsInstallDir /PropertyGroup /Project后面的Microsoft.Cpp.props和Microsoft.Cpp.targets并不会重新定义VCToolsInstallDir它们只是继续在这个基路径上拼接bin\Hostx64\x64、include、lib这些子目录。所以本质上MSVC编译器最终在哪就由两个属性决定VCToolsVersion工具链子目录的版本号名称VCToolsInstallDir工具链安装的根路径搞清楚这一层自定义工具集的思路就通了Toolset.props负责定义“这个工具集叫什么名字、用哪个版本号”Toolset.targets负责告诉MSBuild“去哪个绝对路径找那套工具链”。2. 自定义工具集目录的写法Toolset.props/targets怎么改动在复制文件之前先说一下为什么我坚持“自定义一个新工具集”而不是“直接改v143”。原因有三点第一直接改系统的v143目录VS升级或组件修复时会被覆盖。第二你改的是所有项目的默认行为任何依赖标准v143环境的项目都会被你影响。第三也是最重要的一点直接改没法体现出“工具集”这个抽象层价值——如果哪天又想切回标准v143你难道还把文件翻出来改回去自定义工具集的成本其实非常低核心只需要一个目录、两个XML文件。2.1 推荐使用用户级MSBuild目录而不是系统目录创建自定义工具集时文件既可以放在VS安装目录的MSBuild\Microsoft\VC\v170\Platforms\...下也可以放在用户级目录%LocalAppData%\Microsoft\MSBuild\Microsoft\VC\v170\Platforms\x64\PlatformToolsets\推荐用户级目录。原因很简单不需要管理员权限不会因为VS组件更新而丢失也不影响其他开发者的机器环境。如果你是给团队维护公共构建机那再考虑统一放到系统级目录。2.2 一个最小可用的Toolset.props长什么样我以“让新版MSBuild调用VS2017自带的14.16.27023编译器”为例给你看一份我实际在用的Toolset.propsProject xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 PropertyGroup PlatformToolsetv141_legacy/PlatformToolset VCToolsVersion14.16.27023/VCToolsVersion WindowsTargetPlatformVersion10.0.19041.0/WindowsTargetPlatformVersion /PropertyGroup /Project注意几个点PlatformToolset的值必须和你目录名保持一致否则MSBuild会去解析另一个目录产生“未安装工具集”的报错。VCToolsVersion填的是VS2017工具链在VC\Tools\MSVC\下的实际子目录名也就是14.16.27023。这个数字不是随便写的你可以到老版本VS的安装目录里看真实文件夹名。WindowsTargetPlatformVersion这一项是可选的但强烈建议加。老编译器配特别新的Windows SDK容易出问题锁一个稳定的SDK版本能省很多事。2.3 Toolset.targets里的绝对路径是核心接着是Toolset.targets这份文件的核心就是重定向VCToolsInstallDirProject xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 PropertyGroup VCToolsInstallDirC:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\/VCToolsInstallDir /PropertyGroup /Project写绝对路径时有两点经验第一结尾一定要带反斜杠。MSBuild在后面拼接子路径时用的是直接拼接少了反斜杠会拼出一个非法路径。第二这里到底要不要再设置VCInstallDir如果你的构建链路中还有其他东西引用VCInstallDir比如某些自定义的.props里写了$(VCInstallDir)include最好把它一起指过去。标准MSVC库和工具链的默认查找路径仍是以VCToolsInstallDir为主但补一个VCInstallDir可以让整个环境变量体系更自洽PropertyGroup VCInstallDirC:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\/VCInstallDir VCToolsInstallDir$(VCInstallDir)/VCToolsInstallDir /PropertyGroup注意在VS2015v140及更早版本中工具链目录结构不是VC\Tools\MSVC\14.0\bin这种带版本层级的格局而是直接放在VC\bin\Hostx64\x64下。这意味着如果目标是v140工具链仅靠VCToolsInstallDir还不够通常需要额外调整ExecutablePath、IncludePath、LibraryPath等属性。我建议先把目标锁定在VS2017/VS2019这一代带有Tools\MSVC\version层级的工具链上v140单独开题。3. 从复制到跑通的六个步骤与最小可运行示例这一节给你一套可以照着做的完整流程避免你在过程中反复试错。3.1 准备工具链本体确认老编译器已安装如果你机器上还没有VS2017或VS2019的Build Tools先用vs_buildtools.exe离线安装。命令行安装时指定组件即可例如vs_buildtools.exe --installPath C:\BuildTools --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows10SDK.19041 --quiet --wait安装完成后确认C:\BuildTools\VC\Tools\MSVC\下存在类似14.16.27023这样的目录并且bin\Hostx64\x64\cl.exe、include、lib都在。这套完整目录就是自定义工具集要指向的目标。3.2 按平台创建工具集目录建议先在x64平台下跑通再补其他平台。创建以下目录%LocalAppData%\Microsoft\MSBuild\Microsoft\VC\v170\Platforms\x64\PlatformToolsets\v141_legacy\如果是32位工程还需要%LocalAppData%\Microsoft\MSBuild\Microsoft\VC\v170\Platforms\Win32\PlatformToolsets\v141_legacy\一个最容易忽略的点是如果你只建了x64目录却在Win32工程里指定v141_legacyMSBuild照样报“未安装工具集”。因为发现机制是按Platform分别查目录的两个平台互不相通。3.3 写入两个XML文件在v141_legacy目录下新建Toolset.props和Toolset.targets内容就按上面2.2和2.3节给的最小版本写。这里的文件名必须是Toolset.props和Toolset.targets大小写都不能错。3.4 在vcxproj里切换工具集打开你的工程文件找到PlatformToolset节点PropertyGroup LabelGlobals ProjectGuid{你的GUID}/ProjectGuid KeywordWin32Proj/Keyword RootNamespaceMyLegacyProject/RootNamespace PlatformToolsetv141_legacy/PlatformToolset WindowsTargetPlatformVersion10.0.19041.0/WindowsTargetPlatformVersion /PropertyGroup如果你不想改vcxproj也可以通过命令行覆盖msbuild MyProject.vcxproj /p:PlatformToolsetv141_legacy /p:ConfigurationRelease3.5 第一次构建观察MSBuild是否真的走了自定义目录执行构建后用/v:diag级别的日志抓一下关键属性msbuild MyProject.vcxproj /p:PlatformToolsetv141_legacy /p:ConfigurationRelease /v:diag build_log.txt在日志里搜索VCToolsInstallDir你应当看到它指向C:\BuildTools\VC\Tools\MSVC\14.16.27023\而不是VS2022自己的VC\Tools\MSVC\14.3x\。看到这一条就说明整个链路已经通了。3.6 验证版本让编译器自己报上名来构建结束后在工程里加一个临时cpp文件运行如下代码#include stdio.h int main() { printf(_MSC_VER %d\n, _MSC_VER); return 0; }VS2017工具的_MSC_VER是1916VS2019是1920系列VS2022是1930系列。如果输出值与预期一致工具集切换就是成功的。4. 验证时必须检查的三个维度光跑通不算数很多人在自定义工具集跑通后会直接收手。我觉得至少还要从下面三个维度做一次系统检查否则后续集成到CI还是会翻车。4.1 维度一编译器路径是否真的来自老工具链有人会犯“日志里显示OK实际调的还是新编译器”的错。别只看VCToolsInstallDir还要抓CL任务实际调用的cl.exe路径。在/v:diag日志里搜索CL.exe或CLToolPath确认最终路径带14.16.27023这种老版本号。可靠的做法是直接在这个工程里加一条AdditionalOptions例如ItemDefinitionGroup ClCompile AdditionalOptions/Bv %(AdditionalOptions)/AdditionalOptions /ClCompile /ItemDefinitionGroup/Bv是MSVC的“显示编译器实际路径和版本”开关构建时会输出类似C:\BuildTools\VC\Tools\MSVC\14.16.27023\bin\Hostx64\x64\cl.exe这一条串口输出的可信度最高。4.2 维度二Windows SDK版本是否与老编译器兼容老编译器配合过新的Windows SDK尤其10.0.22621之后的版本在某些涉及WinSDKVersion宏和新增API头文件检查时会碰出奇怪错误。所以强烈建议把WindowsTargetPlatformVersion固定在一个历史版本上。具体选哪个版本取决于你的项目用了哪些API。一般CI上安装10.0.19041.0的SDK兼容性比较广。若项目需要WDDM、新DirectX特性就需要换更高版本。检查方式是看IncludePath和NMakeIncludePath中是否包含你期望的那个SDK版本的ucrt和shared目录。4.3 维度三运行时库依赖是否出现混用老版本工具链生成的二进制默认链接的是它自带CRT库。如果工程里手动混用了新SDK的UCRT或者新版本的vcruntime运行时可能出现“无法定位程序输入点于VCRUNTIME140.dll”这类问题。检查项目里RuntimeLibrary设置默认值MultiThreadedDLL对应动态链接/MD如果你需要静态链接改成MultiThreaded对应/MT关键原则是整个依赖链路尽量都来自同一代工具链。如果某个第三方库是用VS2022编的而你强制整条应用用VS2017工具链编系统找不到匹配的vcruntime版本时会很难排查。这种情况下要么统一升工具集要么确保运行时DLL显式随程序分发。5. 实测踩坑记录Platform目录、路径差异与老SDK兼容自定义工具集踩的坑和普通MSBuild配置的坑不完全一样它更隐蔽。我把实际遇到过的几类问题列出来给你当排障手册。5.1 坑一同一个工具集名称必须每个平台建一次这个坑我提过一次但实际中仍容易漏。有些工程用x64编译但中间步骤会临时生成一个Win32的辅助工程。构建辅助工程时会瞬间报“未安装工具集”你根本想不到是这个原因。应对方案写工具集时直接把Win32、x64、ARM64三个目录都创建好即使暂时用不到。目录内文件一致即可。5.2 坑二绝对路径里的盘符和目录层级差异假设你在一台机器上工具链装在C:\BuildTools\VC\Tools\MSVC\14.16.27023另一台机器装在D:\Program Files\Microsoft Visual Studio\2017\BuildTools\VC\Tools\MSVC\14.16.27023。如果tc.targets里写死绝对路径跨机器必定失败。解决办法是不要直接在.targets里写死绝对路径改成通过环境变量或公共属性注入。例如PropertyGroup LegacyVCToolsInstallDir Condition$(LegacyVCToolsInstallDir)$(MSBuildThisFileDirectory)../../../../../../LegacyVCToolsInstallDir VCToolsInstallDir$(LegacyVCToolsInstallDir)\/VCToolsInstallDir /PropertyGroup但更稳妥的做法是在CI脚本里统一传msbuild MyProject.vcxproj /p:LegacyVCToolsInstallDirD:\VSTools\VC\Tools\MSVC\14.16.27023 /p:PlatformToolsetv141_legacy这样工具集文件本身保持相对稳定机器差异由构建环境注入。5.3 坑三Windows SDK路径和工具集路径互相覆盖Toolset.targets里手动指定VCToolsInstallDir之后有些标准属性仍然可能被系统级Microsoft.Cpp.props里的默认值覆盖。典型情况是ExecutablePath里已经包含了$(WindowsSdkDir)bin但老工具链有时需要自己的工具优先。解决办法是在自定义工具集的.targets里追加优先级PropertyGroup ExecutablePath$(VCToolsInstallDir)bin\Host$(PreferredToolArchitecture)\$(Platform);$(ExecutablePath)/ExecutablePath IncludePath$(VCToolsInstallDir)include;$(IncludePath)/IncludePath LibraryPath$(VCToolsInstallDir)lib\$(Platform);$(LibraryPath)/LibraryPath /PropertyGroup不要用覆盖模式要用前置追加。这样既能保证老工具链优先也不至于丢掉Windows SDK自身的路径。5.4 坑四某些构建事件里调用vcvarsall.bat会重新洗牌环境如果工程里有用到自定义构建事件里面顺手调用了vcvarsall.bat那你前面设好的工具集路径很可能会被重置。因为vcvarsall.bat会根据它自己感知到的VS实例重新设置VCINSTALLDIR、PATH等环境变量。处理方式有两个方向要么构建事件里不要调用vcvarsall.bat改用call $(VCToolsInstallDir)..\..\..\..\Auxiliary\Build\vcvars64.bat这种指定路径的方式要么干脆在自定义工具集里禁止这些构建事件对环境变量做全局修改。从可维护性角度我推荐前者因为很多老工程的构建脚本确实依赖vcvars系列环境。5.5 坑五LLVM/Clang工具集是很好的参考样本如果你对整个机制还有不确定的地方建议看看VS自带的LLVM工具集C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Platforms\x64\PlatformToolsets\LLVM\LLVM工具集就是“自定义工具集”的一个现成案例。它的Toolset.props里同样声明了PlatformToolset它的.targets里同样定了VCToolsInstallDir指向LLVM安装目录。把它的文件结构和v143的文件结构对一遍你会更清楚哪些是必须改的哪些只是约定俗成的写法。6. 进阶玩法、局限性与延伸知识这篇技术的价值不只在“能用老编译器”它还能延伸出一些更灵活的用法。6.1 进阶玩法一用同一个工具集切换多个MSVC版本既然路径和版本号是分开控制的你完全可以做一个v141_multi工具集在构建时动态传入VCToolsVersion来切换msbuild MyProject.vcxproj /p:PlatformToolsetv141_multi /p:VCToolsVersion14.15.26726前提是这几个版本的编译器目录都安装在同一个VC\Tools\MSVC\父目录下。如果不在同一个父目录就改成动态注入VCToolsInstallDir。这种方式在老项目的矩阵构建里非常实用一套配置跑多个老工具链版本。6.2 进阶玩法二把自定义工具集用于Qt和CMake场景很多人在Qt里折腾“装完MinGW编译器后怎么安装MSVC编译工具链”其实就是因为MSVC工具链天然依赖MSBuild的PlatformToolset机制不像MinGW那样有一条独立toolchain。理解了自定义工具集后你可以在Qt Creator里手动指定一个Kits把编译器指向老版本cl.exeCMake则通过-T参数指定工具集cmake -G Visual Studio 17 2022 -T v141_legacy -A x64注意-T的值必须和目录名一致。这个用法在做使用libx265、FFmpeg这类需要编译器特性匹配的项目时很有用——老版本编译器对你的MSC检测宏更友好新版本编译器可能因为代码里的#if _MSC_VER 1920分支而漏掉某些优化路径。6.3 局限性不是所有问题都能靠换工具集解决有几点必须说实话。第一老编译器不支持新的Windows SDK头文件。如果你锁定SDK版本只为了迁就编译器但项目又依赖新SDK中的API这本身就是矛盾得优先满足API需求。第二老编译器对C标准的实现是固定的。VS2017的/std:c17支持范围和VS2022的/std:c20完全不是一个量级。如果代码里用到了concepts、ranges这些特性老工具集直接编译失败这不是配置能弥补的。第三ABI兼容性有方向性。大致来说用老编译器生成的二进制可以被新编译器链接反向则不一定可靠。所以自定义工具集适合“锁旧版本”的场景不适合“混版本”的场景。6.4 延伸知识IDE下拉框看不见自定义工具集怎么办有时候你在VS里打开项目属性发现PlatformToolset下拉列表里没有你新建的工具集名称。这并不代表它坏了只是因为IDE的工具集枚举机制和MSBuild的目录发现机制不完全同步。IDE通常依赖VSIX组件或安装器注册信息来填充列表MSBuild则是直接看目录。如果希望IDE直接显示可以把自定义工具集包装成一个VSIX扩展或者在安装时将文件放到VS系统目录下并重新启动VS。绝大多数内部使用场景下命令行和CI足够是不是显示在下拉列表里并不影响构建。我自己实际用下来最舒服的实践是把工具集文件提交到公司公共构建仓库然后写一个Ensure-CustomToolset.ps1脚本在CI初始化阶段把目录和XML文件生成到用户级MSBuild路径下。这样任何新机器只要执行一次脚本就能在构建时识别v141_legacy这个工具集完全不依赖图形界面的配置。最后再多说一句别怕这个方案听起来偏门。MSVC工具链这层设计本来就是可插拔的LLVM工具集能存在你的自定义工具集也就能存在。只要理解了Toolset.props定义版本和默认属性、Toolset.targets重定向路径这两个核心动作剩下的事情都是重复劳动。希望这篇记录能让你少走几步弯路顺利把老编译器纳入新版MSBuild的掌控之中。