
1. 为什么我最终选择了 WSL VSCode 这套组合搞嵌入式开发的人多少都有点环境洁癖尤其是从 STM32 那一套 Keil、IAR 转过来的人第一次接触 ESP32-IDF 的时候大概率会被那套基于 CMake 的构建系统整得有点懵。我最早是在 Windows 原生环境下装 ESP-IDF 的官方安装器一键搞定看起来挺省事但用久了问题就来了Python 环境冲突、路径里带空格导致脚本报错、串口工具和编译工具链抢资源最要命的是每次升级 IDF 版本都像拆炸弹生怕把之前能跑的工程搞崩。后来我把整个开发环境迁到了 WSL2 里配合 VSCode 的 Remote 功能才算真正找到了舒服的姿势。这套方案的核心逻辑其实很简单把 Linux 原生的工具链跑在 WSL 里把编辑和调试的交互留在 VSCode 里两边各干各擅长的事。ESP-IDF 本身就是为 Linux 环境设计的官方文档里绝大多数命令都是 Linux 风格在 WSL 里跑等于回到了它的主场各种依赖、脚本、编译行为都跟官方预期一致踩坑概率直接砍半。这篇文章我以ESP32-S3为例来走一遍完整流程因为 S3 这颗芯片现在用的人越来越多它带 AI 指令扩展、支持 USB OTG、双核 Xtensa LX7做语音、图像类的项目很合适但它的环境配置跟经典 ESP32 有些细节差异比如目标芯片要指定成esp32s3、USB 串口和 JTAG 的用法不太一样这些我都会讲到。适合谁看如果你手上有一台 Windows 电脑想搭一套干净、可复现、不容易崩的 ESP32-S3 开发环境又不想折腾双系统或者虚拟机那这套 WSL VSCode 的方案基本就是为你准备的。哪怕你之前没怎么用过 Linux 命令行跟着走也能搭起来我会把每条命令为什么这么写都讲清楚。2. 环境整体设计与方案选型拆解2.1 为什么是 WSL2 而不是虚拟机或双系统先说清楚 WSL2 到底是什么。它是 Windows 内置的一个轻量级 Linux 运行环境底层用的是真正的 Linux 内核跑在一个精简的虚拟化层上但跟传统虚拟机最大的区别是它跟 Windows 文件系统、网络、进程是打通的。你可以在 WSL 里直接访问 Windows 的盘符Windows 也能通过\\wsl$访问 Linux 里的文件剪贴板、端口转发都是自动的。对比一下三种方案你就明白为什么我选 WSL2方案启动速度资源占用与 Windows 协作工具链兼容性适合场景WSL2秒级低按需分配极好文件/网络互通原生 Linux兼容性最佳日常嵌入式开发传统虚拟机分钟级高固定分配内存一般需要共享文件夹原生 Linux需要完整桌面环境双系统重启切换独占硬件差需要重启原生 Linux对性能极致要求WSL2 最大的优势是无感。我写代码的时候根本感觉不到自己是在两个系统之间切换VSCode 里打开的是 WSL 里的工程目录终端里跑的是 WSL 的 bash但文件管理器里又能直接拖拽 Windows 下载的固件包进去。这种体验是虚拟机和双系统给不了的。注意WSL2 需要 Windows 10 版本 2004 及以上或者 Windows 11。老版本 Windows 只能跑 WSL1而 WSL1 的文件系统性能很差编译 ESP-IDF 会慢到让你怀疑人生所以务必确认系统版本。2.2 为什么用 VSCode 而不是纯命令行或 EclipseESP-IDF 官方其实提供了好几种开发方式纯命令行idf.py、Eclipse 插件、VSCode 插件。我三种都用过最后留在 VSCode 上原因有这么几个。纯命令行最纯粹idf.py build、idf.py flash、idf.py monitor三板斧走天下但问题是代码跳转、函数补全、头文件索引这些全靠自己配写大工程的时候效率太低。Eclipse 那套插件年代感比较强配置繁琐而且跟 WSL 的集成做得不好。VSCode 的 ESP-IDF 官方插件Espressif IDF是 Espressif 自己维护的功能覆盖了配置、编译、烧录、监视、调试全流程而且它天然支持 Remote-WSL可以直接在 WSL 环境里跑插件后端前端 UI 留在 Windows 上完美契合我们的方案。具体来说VSCode 这套组合给我带来的实际好处是代码智能感知基于 compile_commands.json 的 clangd 索引跳转、补全、错误提示都很准比纯命令行的 ctags 强太多。一键操作底部状态栏有芯片型号、串口、编译/烧录/监视按钮不用记命令。调试集成配合 OpenOCD 和 GDB可以直接在编辑器里打断点、看变量、单步这对排查 S3 上的复杂逻辑非常关键。终端一体化VSCode 内置终端直接就是 WSL 的 bash编译输出和代码在同一个窗口不用来回切。2.3 ESP32-S3 相比经典 ESP32 的环境差异很多人搭环境的时候直接照搬 ESP32 的教程结果在 S3 上翻车。这里我提前把关键差异列出来后面实操会逐一对应。第一目标芯片要显式指定。经典 ESP32 默认就是esp32但 S3 必须用idf.py set-target esp32s3来切换否则编译出来的固件烧进去跑不起来。第二USB 接口有两套。S3 有两个 USB 外设一个是 USB-Serial-JTAG直接接芯片的 GPIO19/20一个是 USB OTG可以接摄像头、U 盘等外设。前者可以用来烧录和调试后者是给应用用的。环境配置时要注意串口设备名USB-Serial-JTAG 在 Linux 下通常识别成/dev/ttyACM0而外接的 USB 转串口芯片比如 CP2102是/dev/ttyUSB0两者别搞混。第三OpenOCD 配置不同。S3 内置了 JTAG 功能用 USB-Serial-JTAG 就能调试不需要额外的 JTAG 调试器但 OpenOCD 的配置文件要用board/esp32s3-builtin.cfg跟经典 ESP32 用的不一样。第四工具链版本要求。S3 需要 ESP-IDF v4.4 及以上版本才支持早期版本没有 S3 的芯片定义。我建议直接用 v5.x 的稳定版功能最全。3. WSL2 安装与基础环境配置实操3.1 开启 WSL 功能并安装 Ubuntu第一步是在 Windows 上把 WSL 功能打开。以管理员身份打开 PowerShell执行wsl --install这条命令在 Windows 11 和较新的 Windows 10 上会自动完成三件事启用适用于 Linux 的 Windows 子系统和虚拟机平台两个可选功能、下载并安装 WSL2 内核、安装一个默认的 Ubuntu 发行版。执行完重启电脑第一次启动 Ubuntu 会让你设置用户名和密码这个密码是 sudo 用的记牢。如果你系统比较老wsl --install不支持那就手动来dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart重启之后去下载 WSL2 内核更新包装上然后wsl --set-default-version 2把默认版本设成 2再从 Microsoft Store 装 Ubuntu。提示安装完用wsl -l -v确认一下版本VERSION 那列必须是 2。如果是 1用wsl --set-version Ubuntu 2转换转换过程可能要几分钟。3.2 换源与基础依赖安装Ubuntu 装好后第一件事是换软件源不然apt下载速度能让你泡杯茶回来还没装完。我用的是清华源编辑/etc/apt/sources.listsudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo sed -i s/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g /etc/apt/sources.list sudo sed -i s/security.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g /etc/apt/sources.list sudo apt update sudo apt upgrade -y然后装 ESP-IDF 编译需要的基础依赖。这些包看着多但每一个都有用缺了会在编译时报各种奇怪的错sudo apt install -y git wget flex bison gperf python3 python3-pip python3-venv \ cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \ build-essential逐个说一下关键包的作用flex和bison是语法分析器生成工具编译某些组件时会用到gperf是完美哈希函数生成器ccache是编译缓存能大幅加速重复编译libusb-1.0-0是 OpenOCD 跟 USB 设备通信的底层库dfu-util用于 USB DFU 模式烧录。这些在官方文档里是分散提到的我一次性列全省得你装到一半发现缺东西。3.3 串口设备在 WSL 里的映射问题这是 WSL 方案里最容易卡住的地方。WSL2 默认不能直接访问 Windows 的串口设备因为它是跑在虚拟化层里的物理 USB 设备不会自动透传进去。解决办法有两个。方案一用usbipd把 USB 设备转发进 WSL。先在 Windows 上装 usbipd-win可以从 GitHub 的 releases 页面下载 msi 安装包然后在 PowerShell 里usbipd list usbipd bind --busid 你的设备BUSID usbipd attach --wsl --busid 你的设备BUSID绑定之后WSL 里就能看到/dev/ttyUSB0或/dev/ttyACM0了。这个方案的好处是烧录、监视、调试全都能在 WSL 里完成体验最统一。方案二在 Windows 侧烧录。也就是编译在 WSL 里做烧录用 Windows 版的 esptool。这个方案简单但每次要手动把固件路径从 WSL 拷到 Windows比较割裂我不推荐。我选方案一。有个细节要注意usbipd attach在每次设备重新插拔后都要重新执行因为绑定关系不是永久的。可以写个批处理脚本一键 attach省得每次手动敲。注意如果usbipd list里设备状态是 Not shared要先usbipd bind一次如果显示 Shared 但没 attach直接 attach 即可。另外 WSL 里访问串口需要权限把当前用户加到 dialout 组sudo usermod -aG dialout $USER然后重启 WSL 生效。4. ESP-IDF 安装与 VSCode 集成全流程4.1 用官方脚本安装 ESP-IDFESP-IDF 的安装我推荐用官方提供的install.sh脚本它会自动下载对应版本的交叉编译工具链、Python 虚拟环境和各种依赖比手动 clone 再配环境变量靠谱得多。先选一个目录放 IDF我习惯放在~/espmkdir -p ~/esp cd ~/esp git clone -b v5.1.2 --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh esp32s3注意最后那个esp32s3参数它告诉脚本只安装 S3 需要的工具链能省下好几个 G 的空间。如果你以后还要玩经典 ESP32 或 C3可以写成./install.sh esp32,esp32s3,esp32c3一次装全。--recursive这个参数很关键因为 ESP-IDF 依赖大量子模块比如 mbedtls、lwip、各种组件不加这个参数 clone 下来是残缺的编译时会报找不到头文件。如果已经 clone 了但忘了加进目录执行git submodule update --init --recursive补上。安装过程会下载工具链国内网络可能比较慢。如果卡住可以设置一下镜像环境变量再重试export IDF_GITHUB_ASSETSdl.espressif.com/github_assets ./install.sh esp32s34.2 激活环境与验证安装安装完成后每次开新终端都要激活环境也就是把 IDF 的路径、工具链、Python 虚拟环境加到当前 shell 里. $HOME/esp/esp-idf/export.sh注意前面那个点号加空格是 source 的简写不能漏。执行成功的话会打印一堆环境变量设置信息最后提示 Done。验证一下idf.py --version能打印出版本号就说明装好了。如果提示 command not found八成是 export.sh 没 source 成功检查路径对不对。提示每次都手动 source 太麻烦可以在~/.bashrc末尾加一个别名alias get_idf. $HOME/esp/esp-idf/export.sh以后敲get_idf就行。但别直接把 export.sh 写进 .bashrc 自动执行因为它会改变 PATH 和 Python 环境可能影响你系统里其他 Python 项目。4.3 VSCode 安装与 Remote-WSL 配置Windows 侧去官网下载 VSCode 装上安装时勾选添加到 PATH和将通过 Code 打开操作添加到目录上下文菜单方便后续使用。装完后第一件事是装WSL 扩展全名 WSLMicrosoft 出品。装好之后左下角会出现一个绿色的远程连接图标点它选 Connect to WSLVSCode 就会连到你的 Ubuntu 环境里。这时候你再打开终端默认就是 WSL 的 bash插件也分成了本地和WSL两套需要装到 WSL 侧的插件要单独装。接着在 WSL 侧装Espressif IDF扩展。这个扩展是 Espressif 官方维护的装的时候注意看它是不是装在 WSL: Ubuntu 这个远程环境里别装到 Windows 本地去了。装好后按F1输入ESP-IDF: Configure ESP-IDF extension选择 Advanced 模式然后填三个路径ESP-IDF 路径/home/你的用户名/esp/esp-idf工具链路径/home/你的用户名/.espressifPython 路径/home/你的用户名/.espressif/python_env/idf5.1_py3.10_env/bin/python填完点 Install扩展会自己校验路径并生成配置。这一步如果路径填错后面编译会一直报找不到工具链。4.4 创建第一个 ESP32-S3 工程并编译烧录环境配好了来跑个 Hello World 验证。用 VSCode 命令面板F1执行ESP-IDF: Show Examples Projects选get-started里的hello_world然后选一个目录创建工程。创建完打开工程先设置目标芯片。在 VSCode 底部状态栏点芯片型号那个按钮选esp32s3或者在终端里idf.py set-target esp32s3这一步会重新生成 sdkconfig 和 build 目录是 S3 工程必须做的。然后编译idf.py build第一次编译会比较久几分钟因为要编译整个 IDF 组件库之后有 ccache 加速就快了。编译成功会生成build/hello_world.bin。烧录前确认串口。WSL 里ls /dev/tty*看一下S3 的 USB-Serial-JTAG 一般是/dev/ttyACM0。然后idf.py -p /dev/ttyACM0 flash monitorflash烧录monitor打开串口监视器。看到 Hello world! 打印出来环境就算通了。退出监视器按Ctrl]。注意如果烧录时报 Failed to connect先检查 usbipd 有没有 attach再检查串口权限。S3 进下载模式有时需要按住 BOOT 键再按 RESET松开 RESET 再松开 BOOT尤其是第一次烧录或者固件跑飞的时候。5. 常见问题排查与避坑经验实录5.1 编译类问题速查环境搭起来之后真正折磨人的往往是各种编译报错。我把踩过的坑整理成一张表遇到问题先对照查。报错信息根本原因解决方法CMake Error: The source directory ... does not exist工程路径含空格或中文把工程移到纯英文无空格路径fatal error: xxx.h: No such file or directory子模块没拉全git submodule update --init --recursivePython interpreter not found虚拟环境路径配错重新配置 IDF 扩展的 Python 路径ccache: command not found没装 ccachesudo apt install ccacheregion iram0_0_seg overflowed代码太大超出 IRAM优化代码或调整内存布局undefined reference to ...组件依赖没声明在 CMakeLists.txt 的 REQUIRES 里加组件名重点说两个。第一个是路径问题ESP-IDF 的构建系统对路径里的空格和中文极其敏感我见过太多人把工程放在我的文档下面然后编译失败。养成习惯所有嵌入式工程都放在纯英文路径下比如~/esp/projects/。第二个是 IRAM 溢出。S3 的 IRAM 只有 512KB如果开了大量中断处理、DMA 缓冲区很容易爆。解决办法是把不要求极致性能的函数放到 flash 里执行用IRAM_ATTR宏只标注真正需要常驻内存的函数。这个坑我在做一个音频采集项目时踩过当时把整个 FFT 库都标了 IRAM_ATTR结果直接溢出后来只标了中断服务函数才解决。5.2 烧录与串口类问题烧录环节的问题通常跟 USB 设备转发有关。最常见的现象是idf.py flash卡在 Connecting... 然后超时。排查顺序是这样的先确认usbipd list里设备是不是 Attached。如果显示 Shared 但没 attach执行usbipd attach --wsl --busid BUSID。如果 attach 了但 WSL 里看不到设备检查 WSL 内核版本老版本内核对某些 USB 转串口芯片支持不好wsl --update升级一下。再确认串口权限。ls -l /dev/ttyACM0看属主和属组如果当前用户不在 dialout 组里会报 Permission denied。加组之后一定要完全关闭 WSL 再重开wsl --shutdown光重启终端不生效。还有一个隐蔽的坑Windows 侧可能有程序占用了串口比如你之前开着的串口助手、Arduino IDE 的串口监视器。这些程序会锁住设备导致 usbipd attach 失败或者 WSL 里读不到数据。烧录前把所有可能占用串口的软件关掉。提示S3 用 USB-Serial-JTAG 烧录时如果之前烧进去的固件把 USB 引脚复用成了别的功能会导致 USB 设备直接消失。这时候要手动进下载模式按住 BOOT点一下 RESET松开 BOOT设备会以 ROM 下载模式重新枚举。5.3 调试配置的独家经验S3 内置 JTAG 是它的一大优势不用买额外的调试器就能单步调试。配置 OpenOCD 的时候VSCode 的 ESP-IDF 扩展会自动生成 launch.json但默认配置有时候连不上我总结几个关键点。第一OpenOCD 的配置文件要选对。S3 用board/esp32s3-builtin.cfg这个文件里已经配好了 USB-Serial-JTAG 的接口。如果你用的是外接 JTAG 调试器才需要换成interface/ftdi/xxx.cfg加target/esp32s3.cfg的组合。第二调试和串口监视不能同时用同一个 USB-Serial-JTAG 接口。因为 JTAG 和串口是复用同一个 USB 外设的OpenOCD 占用了之后idf.py monitor就连不上了。我的做法是调试的时候用 OpenOCD 的 GDB 输出看日志或者用另一个 USB 口接串口。S3 有两个 USB 口的话就方便了一个专门调试一个专门看日志。第三断点打在 flash 里的代码上时OpenOCD 需要设置硬件断点。S3 支持有限数量的硬件断点通常 2 个打多了会报 Cannot insert breakpoint。软件断点需要修改 flash 内容调试时尽量少打断点或者用monitor命令配合日志输出。5.4 性能与体验优化技巧环境跑通之后还有一些能显著提升日常开发体验的优化。编译加速确保 ccache 生效。idf.py build时如果看到 ccache hit 字样就说明在用了。可以在 menuconfig 里把Compiler options下的Enable ccache打开。另外 WSL2 的文件系统性能对编译影响很大工程一定要放在 WSL 的 Linux 文件系统里比如~/esp/projects不要放在/mnt/c/下面。因为跨文件系统访问要走 9P 协议IO 性能差好几倍编译一个大工程能差出好几分钟。内存分配WSL2 默认会占用最多一半的物理内存如果你机器内存不大比如 8G编译时可能触发 OOM。可以在 Windows 用户目录下建.wslconfig文件限制[wsl2] memory6GB processors4 swap2GB改完wsl --shutdown重启生效。终端体验WSL 里默认的终端配色和字体一般可以在 VSCode 设置里调。另外装个zsh加oh-my-zsh命令补全和历史搜索体验比 bash 好很多对经常敲idf.py长命令的人很友好。备份环境环境配好之后用wsl --export Ubuntu esp32-env.tar把整个发行版导出备份。哪天环境搞崩了wsl --import一下就能恢复比重装省事太多。这个习惯我强烈建议养成尤其是你花了大半天配好的工具链。6. 从点亮 LED 到跑通第一个外设6.1 用 blink 例程验证完整链路Hello World 只验证了串口输出真正要确认工具链、烧录、GPIO 都正常跑个 blink 更实在。在 VSCode 里创建get-started/blink例程set-target 成 esp32s3然后看代码。blink 例程默认用的是 GPIO 某个引脚S3 开发板上一般板载 LED 接在 GPIO48 或者 GPIO38 上不同开发板不一样看原理图。如果你用的是官方 ESP32-S3-DevKitC板载 RGB LED 是 WS2812接在 GPIO48需要用到 RMT 外设驱动比普通 GPIO 复杂一点。如果只是验证可以外接一个 LED 到任意 GPIO改一下代码里的引脚号就行。编译烧录之后看到 LED 闪烁说明从代码编辑、编译、烧录到硬件执行的完整链路全通了。这一步的意义在于它验证的不只是软件环境还包括 USB 转发、串口通信、芯片复位这些硬件相关的环节比 Hello World 更有说服力。6.2 接入一个 I2C 传感器练手光跑例程不够我建议接一个实际的外设练练手比如常见的 OLED 屏或者温湿度传感器。以 SSD1306 OLED 为例它走 I2C 接口接线简单驱动成熟。S3 的 I2C 引脚可以灵活映射我习惯用 GPIO8 做 SDA、GPIO9 做 SCL。在工程里加一个 I2C 初始化的代码i2c_config_t conf { .mode I2C_MODE_MASTER, .sda_io_num 8, .scl_io_num 9, .sda_pullup_en GPIO_PULLUP_ENABLE, .scl_pullup_en GPIO_PULLUP_ENABLE, .master.clk_speed 400000, }; i2c_param_config(I2C_NUM_0, conf); i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);这段代码里几个参数值得说。clk_speed设 400kHz 是 I2C 的快速模式SSD1306 支持如果线比较长或者干扰大降到 100kHz 更稳。pullup_en打开内部上拉如果你模块上已经有外部上拉电阻这里可以关掉避免上拉过强。i2c_driver_install的后三个参数是接收/发送缓冲区大小和中断标志主机模式不需要缓冲区填 0 即可。接上 OLED 之后用现成的esp_lcd组件或者第三方 SSD1306 驱动库就能显示文字了。这个过程会让你熟悉 ESP-IDF 的组件管理机制——怎么在idf_component.yml里声明依赖、怎么用idf.py add-dependency添加组件。这套机制比手动拷贝源码优雅得多是 IDF 相比其他嵌入式框架的一大优势。6.3 关于 ESP32-S3 摄像头应用的延伸热词里提到了esp32-s3 ov5640 驱动说明不少人是冲着 S3 的摄像头能力来的。S3 支持摄像头接口LCD_CAM 外设可以驱动 OV2640、OV5640 这类并口摄像头。这里简单提一下环境层面的注意点因为摄像头应用对内存和引脚配置要求比较高。OV5640 是 500 万像素数据量大S3 的 PSRAM 必须开。在 menuconfig 里Component config - ESP PSRAM打开模式选 Octal如果用的是带 Octal PSRAM 的模组比如 ESP32-S3-WROOM-1-N16R8。摄像头数据通过 DMA 直接搬到 PSRAM不占 IRAM这是能跑起来的关键。引脚方面S3 的摄像头接口引脚是固定的不能随便映射具体看esp32-camera组件的示例。XCLK、PCLK、VSYNC、HSYNC 加上 8 位数据线一共十几个引脚接线要仔细。环境配置上摄像头应用需要把esp32-camera作为组件加进来用idf.py add-dependency espressif/esp32-camera就行。这块内容展开能写一整篇这里点到为止。核心意思是WSL VSCode 这套环境搭好之后往上叠摄像头、音频、AI 这些应用都是顺理成章的事底层工具链不用再动。7. 我在这套环境里踩过的几个真实坑说几个文档里不会写、但实际会遇到的坑。第一个是WSL 的时钟漂移。WSL2 在电脑休眠唤醒后系统时间有时会跟真实时间差几秒甚至几分钟。这个对 ESP-IDF 影响很大因为构建系统靠文件时间戳判断哪些文件需要重新编译时间错乱会导致该重编的不重编、不该重编的反复重编。解决办法是sudo hwclock -s同步一下或者干脆wsl --shutdown重启。我现在的习惯是每天开工前先wsl --shutdown一次干净利落。第二个是usbipd 的 attach 状态丢失。电脑重启、WSL 重启、USB 设备重新插拔都会导致 attach 失效。我写了个 PowerShell 脚本开机自动执行 bind 和 attach省得每次手动弄。脚本内容大概是这样$busid (usbipd list | Select-String CP2102|USB Serial|JTAG | ForEach-Object { ($_ -split \s)[0] }) usbipd bind --busid $busid usbipd attach --wsl --busid $busid第三个是VSCode 扩展装错位置。Remote-WSL 模式下插件分本地和远程两套。ESP-IDF 扩展必须装在 WSL 侧因为它要调用 WSL 里的工具链。我有次手滑装到了本地结果配置的时候怎么都找不到 IDF 路径折腾半天才发现装错地方了。判断方法很简单看扩展面板里那个扩展是不是显示在 WSL: Ubuntu 分组下。第四个是Python 版本冲突。ESP-IDF 自带的 Python 虚拟环境跟系统 Python 是隔离的但如果你在 shell 里先激活了别的 venv再 source export.sh可能会串环境。我的做法是 export.sh 之前先deactivate掉所有其他虚拟环境保证 IDF 用的是它自己的 Python。这套环境我从 v4.4 用到 v5.1中间升级过几次 IDF 版本整体很稳。升级的时候注意新版本 IDF 可能需要重新跑install.sh装新工具链旧的 build 目录最好删掉重新生成避免 CMake 缓存导致的诡异错误。每次升级前用wsl --export备份一下出问题能秒回滚这个习惯救过我好几次。