☰
VS2022中自定义PlatformToolset调用旧版MSVC构建老项目
2026/9/28 5:42:39 网站建设 项目流程

老项目卡在旧编译器上,新电脑又只装了 VS2022,这大概是不少维护老代码库的人都撞过的墙。打开工程一看,平台工具集是 v120(VS2013),但机器上只有 v143,编译器版本差了好几个代际,直接编译就是一堆C1189、C1083之类的报错,根本走不动。装一整台旧版 Visual Studio 又太重,而且新版 Windows SDK 对老编译器也不友好。我当时的出路,就是基于 VS2022 里现有的 v143 PlatformToolset 目录结构,硬造了一个自定义工具集,让它去调用老版 MSVC 的 cl.exe、link.exe 和配套库文件,最终把项目完整跑通。

这篇东西不是讲怎么破解,也不是绕开授权,核心假设是你手里本来就有一个可以合法使用的老版 MSVC 编译工具链,只是不知道怎么让它被新版 MSBuild 调起来。本文会从 v143 的 Toolset.props 和 Toolset.targets 到底在做什么开始拆,再到具体怎么复制、改名、改路径、覆盖工具链目录,最后是排查坑位,把这套“自定义工具集”的路子完整走一遍。

1. 先搞清楚 PlatformToolset 的底细,为什么能这么改

1.1 从 MSBuild 视角看 PlatformToolset

在 .vcxproj 文件里经常能看到这样的配置:

<PropertyGroup Label="Globals"> <ProjectGuid>{...}</ProjectGuid> <Keyword>Win32Proj</Keyword> <RootNamespace>MyApp</RootNamespace> <WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion> <PlatformToolset>v143</PlatformToolset> </PropertyGroup>

PlatformToolset在 MSBuild 里就是一个普通的属性,但 Microsoft.Cpp.Default.props 会拿着这个属性值去拼路径,找到对应的工具集定义目录。大致搜索逻辑是:

$(VCTargetsPath)\Platforms\$(Platform)\PlatformToolsets\$(PlatformToolset)\

其中$(VCTargetsPath)通常指向:

C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\

$(Platform)就是Win32、x64、ARM64这些。

这个目录下面会放Toolset.props和Toolset.targets。MSBuild 在导入公共 C++ 属性之前,会先找到这个目录,把Toolset.props导入进来,随后在目标执行阶段导入Toolset.targets。正是因为 MSBuild 只认“目录名”和“文件路径”,没有做任何注册表或系统级别的强校验,所以我们完全可以自己新建一个目录,放一份修改过的 props/targets,让它变成一个全新的工具集名字。

这就像给 Windows 装驱动一样,系统不关心驱动是谁写的,只关心有没有符合约定的接口。PlatformToolset 的接口就是 Toolset.props 和 Toolset.targets 这两个文件。

1.2 v143 的 Toolset.props 和 Toolset.targets 拆解

以 VS2022 17.x 为例,v143 的 Toolset.props 内容大体是这样(不同小版本会有些差异,但骨架一致):

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(VCToolsInstallDir)\Auxiliary\Build\Microsoft.VCToolsVersion.props" Condition="'$(VCToolsInstallDir)' != '' and exists('$(VCToolsInstallDir)\Auxiliary\Build\Microsoft.VCToolsVersion.props')" /> <PropertyGroup> <PlatformToolsetVersion>143</PlatformToolsetVersion> <VCDir>$(VCInstallDir)</VCDir> <SkipVcPkg>false</SkipVcPkg> <ToolsetPropsImported>true</ToolsetPropsImported> <ToolsetTargetsImported>true</ToolsetTargetsImported> </PropertyGroup> <PropertyGroup> <VCToolsInstallDir Condition="'$(VCToolsInstallDir)' == ''">$(VCInstallDir)\Tools\MSVC\$(LatestInstalledVCVersion)</VCToolsInstallDir> </PropertyGroup> </Project>

关键在于最后那个VCToolsInstallDir,它最终指向的是一整套 MSVC 工具链的安装根目录,比如:

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.38.33130\

这个根目录下有bin、include、lib、atlmfc等子目录,CL、Link、LIB、RC 这些可执行文件路径都是基于$(VCToolsInstallDir)拼接出来的。比如微软 C++ 编译任务的ToolPath属性,最终会类似:

$(VCToolsInstallDir)\bin\HostX64\x64\cl.exe

Toolset.targets 这边做的事更偏向“定义工具包路径和任务入口”,它会导入Microsoft.Cpp.Common.props、定义CLToolPath、LinkToolPath之类的属性,并把这些属性传给对应的 MSBuild Task。

看明白这个结构,思路就清晰了:v143 这套平台工具集的逻辑,实际上是一套“路径模板 + 版本号标识”。只要保持整个 props/targets 编写逻辑不变,把VCToolsInstallDir指向老版 MSVC 的目录,再把PlatformToolsetVersion改成对应版本,MSBuild 就会老老实实地用老编译器和老库文件干活。

2. 核心思路:复制 v143 改造成自定义工具集

2.1 三条路线的取舍

要引入自定义工具集,有几种做法,这里先做一个对比。

方案侵入性持久性IDE 兼容适合场景
直接改系统 v143 目录高,影响所有项目差,VS 修复/升级会覆盖一般临时调试,不推荐
复制 v143 成新工具集再改路径低,只在新增目录里操作好,独立目录不受官方更新影响较好,项目文件指定后 IDE 可构建长期维护,推荐
用 Directory.Build.props 全局覆盖属性中,影响目录下所有项目中,依赖 props 导入顺序差,IDE 下拉框不识别临时切换编译器快速验证

直接改官方目录这事,我试过一次就放弃了。费半天劲改完,VS 装了个更新,整个目录被还原,等于白干。唯一的优点是改起来最快,适合我刚才说的临时调试,干完就重装工具集也不是不行,但作为长期方案实在不合格。

2.2 为什么推荐“复制 v143 + 覆盖路径”

复制官方 v143 目录生成一个独立的新工具集,比如起名LegacyVC120,有以下几点好处:

  • 不改动官方目录,VS 自身更新不会覆盖自定义目录。
  • 新工具集目录里只用复制那两三个 props/targets 文件,体积很小,方便用 Git 管理。
  • MSBuild 只认目录名,不关心目录底层调用的是哪个版本的编译器。我们改的是路径,编译器任务本身的调度逻辑还是沿用 v143 的,这意味着属性传递、依赖分析、增量编译这些机制天然就能正常工作。

实际开发中我从来不直接改 Toolpath 去适配老版编译器的特殊布局,而是反过来,把老版编译器的文件整理成“接近 v143 布局”的目录,再让VCToolsInstallDir指过去。这样付出的改造工作量最小,维护成本也最低。

2.3 新版行为还是老版行为?版本号怎么填

PlatformToolsetVersion这个属性会影响微软官方 props 中的不少条件分支。比如Microsoft.Cpp.Default.props里会根据_ToolsetVersion去做一些兼容性判断,像是要不要定义_MSC_VER相关的宏、默认链接哪些库等。如果你给自定义工具集填 143,MSBuild 行为会更接近 v143,但这也意味着某些老工具的默认行为可能对不上。我的习惯是:能用新版行为路径就没必要特意改成老版号,只有遇到具体条件分支报错时,再回头把PlatformToolsetVersion改成老版本号(比如 120)试一把。这里面没有标准答案,完全取决于目标项目对老编译器行为的依赖程度。

3. 动手前必须准备好的一副牌:老版 MSVC 工具链

3.1 老版 cl.exe 不是孤军奋战

先泼一盆冷水:老版 MSVC 编译器不是拷贝一个cl.exe就能跑的。它依赖的东西通常包括:

  • bin目录下的可执行文件:cl.exe、link.exe、lib.exe、ml.exe等。
  • 运行支持库:mspdb*.dll、mspft*.dll、mspbi*.dll这些 DLL,通常也在bin目录里。
  • include目录:C/C++ 标准库头文件,还有stdint.h、intrin.h这些编译器内建头文件。
  • lib目录:libcmt.lib、libcmtd.lib、oldnames.lib等静态库和导入库。
  • 如果项目用 MFC/ATL,还需要atlmfc的 include/lib。
  • 如果项目用到rc.exe,那通常来自 Windows SDK,不属于 MSVC 工具链自身,但路径拼接也要管。

以 VS2013 的 v120 为例,典型目录结构是:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\ ├─ bin\ (cl.exe, link.exe, mspdb120.dll ...) ├─ include\ (标准头文件) ├─ lib\ (静态库和导入库) └─ atlmfc\ (MFC 相关)

而从 VS2015 的 v140 开始,布局变成了嵌套结构:

...\VC\Tools\MSVC\14.0.24215\ ├─ bin\HostX64\x64\cl.exe ├─ include\ └─ lib\

v140 这种布局和 v143 几乎一致,所以在自定义工具集里覆盖路径时,基本不用做额外修改。v120 这种老布局则要多一层适配。

3.2 怎么搞到一套可移植的老版工具链

获取老版工具链,我常用的方法大致有两种:

第一种是从装了老版 Visual Studio 的机器上,把整个VC目录复制出来。只要目录里自带所有 DLL,脱离原安装环境也能工作。需要留意的是,有些老版组件依赖 VC 安装目录之外的公共文件,比如C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE里的某些 DLL,但 C++ 命令行编译要的绝大多数东西都在 VC 目录内,复制之后先跑一次cl.exe自测一下最稳妥。

第二种是用vs_buildtools.exe在离线场景下安装相应的“MSVC v140 构建工具”之类的组件,然后把安装出来的工具链目录整体挪走。如果你只是想给老项目提供一个构建环境,又不想留着一整套旧 VS,这种方法比较干净。

拿到工具链之后,强烈建议放到一个没有空格、没有中文的目录,比如:

D:\LegacyToolchains\MSVC12 ├─ bin\ ├─ include\ ├─ lib\ └─ atlmfc\

路径里一旦有空格,MSBuild 属性拼接和命令行参数解析就很容易出幺蛾子。这是最不值得踩的坑,但往往就有人踩上去。

4. 实操:创建 LegacyVC120 工具集并跑通构建

4.1 找到并复制官方 v143 目录

以 VS2022 Community 为例,在安装目录下找到:

C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Platforms\Win32\PlatformToolsets\v143\ C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Platforms\x64\PlatformToolsets\v143\

如果你本机装的就是 Professional 或 Enterprise,把路径里的 Community 对应替换掉即可。在PlatformToolsets\v143同级目录下新建一个文件夹,名字建议和最终工具集名字保持一致。比如工具集想叫LegacyVC120,就建:

Win32\PlatformToolsets\LegacyVC120\ x64\PlatformToolsets\LegacyVC120\

把 v143 目录下的Toolset.props和Toolset.targets分别复制过来。如果你的项目还跑 ARM64,对应的 ARM64 目录也照做。

4.2 修改 Toolset.props,把 VCToolsInstallDir 指到老目录

打开Win32\PlatformToolsets\LegacyVC120\Toolset.props,修改成类似下面的内容:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(VCToolsInstallDir)\Auxiliary\Build\Microsoft.VCToolsVersion.props" Condition="'$(VCToolsInstallDir)' != '' and exists('$(VCToolsInstallDir)\Auxiliary\Build\Microsoft.VCToolsVersion.props')" /> <PropertyGroup> <PlatformToolsetVersion>120</PlatformToolsetVersion> <VCDir>$(VCInstallDir)</VCDir> <ToolsetPropsImported>true</ToolsetPropsImported> </PropertyGroup> <PropertyGroup> <VCToolsInstallDir Condition="'$(VCToolsInstallDir)' == ''">D:\LegacyToolchains\MSVC12</VCToolsInstallDir> </PropertyGroup> </Project>

那位从官方 v143 文件继承下来的Microsoft.VCToolsVersion.props导入,如果老工具链目录下没有Auxiliary\Build\Microsoft.VCToolsVersion.props,条件判断会直接跳过导入,不影响构建。保留这一行主要是为了兼容新版布局的工具链,比如以后把工具集指向 v140 的VC\Tools\MSVC\14.0.24215目录时,还能自动读取版本信息。

VCToolsInstallDir一旦指向D:\LegacyToolchains\MSVC12,MSBuild 的 CL 任务就会尝试去D:\LegacyToolchains\MSVC12\bin\HostX64\x64\cl.exe找编译器。如果你的老版工具链没有这种 HostX64 结构,就得进入下一步,整理目录布局或者覆盖 tool 路径。

4.3 处理老版目录结构的不兼容

这里有两种选择:改目录结构,或者改 targets。

第一选择是整理老工具链目录,让它模仿 v143 的样子:

D:\LegacyToolchains\MSVC12\ ├─ bin\HostX64\x64\cl.exe ├─ bin\HostX64\x64\link.exe ├─ bin\HostX86\x86\cl.exe ├─ include\ └─ lib\

这里说的“整理”不一定是复制文件,可以用目录符号链接mklink /J把bin下的子目录链接过去。比如把老版 VC 的bin\amd64目录链接成bin\HostX64\x64。这样做的好处是,整个 v143 的路径拼接逻辑完全不用动。

如果确实不想动目录结构,那就要在 Toolset.targets 里覆盖任务属性。比如在 Toolset.targets 末尾加上:

<PropertyGroup> <CLToolPath>$(VCToolsInstallDir)\bin</CLToolPath> <LinkToolPath>$(VCToolsInstallDir)\bin</LinkToolPath> <LibToolPath>$(VCToolsInstallDir)\bin</LibToolPath> </PropertyGroup>

这样修改以后,构建时 CL 和 Link 任务会直接在bin目录下找 cl.exe、link.exe。但这样做有个隐患,就是老版bin里通常没有按 Host 架构区分的子目录,遇到跨平台编译场景可能抓瞎。所以我的建议仍然是:优先整理目录布局,让它对齐 v143,以尽量小的改动量换最大的兼容性。

4.4 在工程文件里启用自定义工具集

方式很简单,打开 .vcxproj,把:

<PlatformToolset>v143</PlatformToolset>

改成:

<PlatformToolset>LegacyVC120</PlatformToolset>

如果不想逐个工程修改,可以在解决方案根目录建一个Directory.Build.props:

<Project> <PropertyGroup> <PlatformToolset>LegacyVC120</PlatformToolset> </PropertyGroup> </Project>

这个文件会被 Microsoft.Cpp.Default.props 之前的早期导入环节读取,所以要确认它对项目文件的优先级是合适的。实测下来,Directory.Build.props 的方式在绝大多数场景下都能生效,唯一的问题就是它影响面是整个目录树,如果目录里混了不同需求的项目,就得小心点。

4.5 命令行构建验证

不用 IDE 打开工程,先用 msbuild 直接在命令行验证:

msbuild MyApp.vcxproj /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=LegacyVC120 /v:minimal /t:Rebuild

关键在于-v:minimal看构建是否通过,然后加上-v:diag看具体的 cl.exe 命令行:

msbuild MyApp.vcxproj /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=LegacyVC120 /v:diag /t:ClCompile

日志里会出现类似:

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.38.33130\bin\HostX64\x64\cl.exe ...

如果你的路径修改生效,应该看到的是:

D:\LegacyToolchains\MSVC12\bin\HostX64\x64\cl.exe ...

看到老路径,说明工具集已经被 MSBuild 正确加载。这时候编译信息里显示的_MSC_VER宏、编译器版本号都会是老版,可以用这些作为验证 Toolset 是否真的在生效的依据。

5. 常见问题和排查技巧实录

5.1 MSB8020:找不到 v143 / 找不到自定义工具集

MSBuild 报MSB8020通常是这个原因:目录名和PlatformToolset属性值不一致,或者路径层级放错了。检查三处:

  • PlatformToolset属性值是否与目录名完全一致,大小写也要一致。
  • 目录是否放在PlatformToolsets下,并且是在正确的Platform子目录里,比如 x64 工程应该读x64\PlatformToolsets。
  • 复制的 props/targets 是否都在目录内,别只复制了 tool.props 少了 tool.targets。

IDE 里平台工具集下拉框看不到自定义项,这个属于正常现象。IDE 的下拉框默认只显示系统注册过的官方工具集。不过这不影响构建,只要 .vcxproj 或 Directory.Build.props 里写了自定义工具集名字,IDE 加载项目后照常可以用 MSBuild 构建。如果 IDE 弹“未安装工具集”的提示,尝试把工程文件里的PlatformToolset值改成自定义名,再重载项目。

5.2 老版 cl.exe 找不到 stdio.h

这里十有八九是 INCLUDE 环境变量没有指向老版工具链的 include 目录。构建时直接设一下:

set INCLUDE=D:\LegacyToolchains\MSVC12\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt set LIB=D:\LegacyToolchains\MSVC12\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64 msbuild MyApp.vcxproj /p:PlatformToolset=LegacyVC120 /p:Configuration=Release /p:Platform=x64

但这里有个先后问题:MSBuild 的 CL 任务默认会把工具集自身的 include/lib 目录通过/I和/LIBPATH传给编译器。如果$(VCToolsInstallDir)没生效,就会传成新版工具链路径,导致老编译器优先找新版头文件,然后因为语法不兼容崩掉。所以最稳的办法不是手动设置环境变量,而是先在Toolset.props里确认VCToolsInstallDir的值真的指向老目录。

5.3 link.exe 报无法打开 mspdbcore.dll / LNK1104

这个坑很典型。老版 link.exe 运行时会加载mspdbcore.dll(不同版本名字不一样,比如 v120 下面是mspdb120.dll),它是链接器的调试数据库支持库。如果这些 DLL 不在 link.exe 同目录,且系统 PATH 里也找不到,链接器就会罢工。

排查方式:先打开命令行,把老工具链bin目录手动添加到 PATH 里,然后手动执行一次 link 命令测一下能不能通过。能通过,说明是 MSBuild 的任务环境没带上 PATH。这时要么在构建脚本里统一设置了 PATH 再调 msbuild,要么直接把对应 DLL 复制到 link.exe 所在目录。我的选择是后者,简单粗暴,一劳永逸。

5.4 新版 Windows SDK 不兼容老编译器

老编译器对 Windows SDK 的版本很敏感。比如 VS2013 的 cl.exe 遇到 10.0.19041 版本的windows.h和sdkddkver.h,有概率出现C1189 #error或一堆_WIN32_WINNT相关的奇怪报错。解决办法是指定项目使用老版 Windows SDK:

<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>

前提是机器上确实安装了 Windows 8.1 SDK,或者把老 SDK 的 Include/Lib 目录也复制到本地。这个属性可以放在 .vcxproj 里,也可以在自定义工具集的 Toolset.props 里统一指定,这样所有用这个工具集的项目都不用单独配。

5.5 常见问题速查表

现象可能原因快速解法
MSB8020 提示找不到工具集工具集目录名与属性值不匹配检查 PlatformToolset 值、目录名、平台子目录
Fatal error C1083: Cannot open include file: 'stdio.h'VCToolsInstallDir未生效或目录名不对检查 Toolset.props 中的路径,确认没有拼写错误
LNK1104: cannot open file 'libcmt.lib'LIB 路径没指到老工具链确认$(VCToolsInstallDir)\lib存在,或在 Toolset.props 中设置 LIB 路径
找不到 mspdb120.dll / mspdbcore.dlllink.exe 缺少运行 DLL把 DLL 放到 link.exe 同目录
C1189 #error: SDK 版本不支持新版 SDK 头文件与老编译器不兼容指定 WindowsTargetPlatformVersion 为旧 SDK
IDE 下拉框不显示自定义工具集IDE 只显示官方注册工具集直接在项目文件里写工具集名,重载项目

6. 实际操作中的几点体会

这套自定义工具集方案,我已经跑了大半年,日常维护一个老内核模块的构建链。最大的体会是“目录布局强迫症”能省很多事。一开始我也试图通过改 Toolset.targets 里的各种 ToolPath 来适配老版目录,结果每次构建都会冒出新的路径问题。后来花了一个下午把老工具链的目录整理成 v143 的形状,之后所有项目都稳定下来,基本没再出过岔子。

再分享一个小技巧:把整个自定义工具集目录纳入 Git 管理,包括Toolset.props、Toolset.targets,以及一份说明文档,记录“这个工具集是从哪个 VS 版本复制来的、原始 v143 目录在哪个路径、改动了哪些属性”。这样换机器、换同事接手时,只需要重新把工具链目录放到指定位置,再克隆一下自定义工具集配置就能恢复构建环境,不需要再去翻旧文档或者试错。

如果你也在做类似的事,建议先从命令行 msbuild 跑一个最小工程验证工具集,再回到 IDE 里打开完整项目。命令行能过滤掉 IDE 的缓存和 UI 层干扰,问题定位起来会快得多。等命令行构建稳定通过,再引入 IDE 场景,基本上就只是看着它自动构建了。

这个自定义工具集的方法还可以往外延伸,不止是接老版 MSVC,哪怕是接一个第三方编译器,或者某个被裁剪过的工具链,只要它兼容 cl.exe 的命令行参数,这套“复制 v143 + 改 VCToolsInstallDir + 对齐目录布局”的思路都能套用。核心永远是:让 MSBuild 用最少的改变,去匹配我们准备好的工具链路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询