这是一篇"从报错到根因,再到稳定落地方案"的复盘文章,记录我在 UE 5.6 项目里编译 Cesium for Unreal(含 cesium-native 外部依赖)时遇到的一组常见但定位成本较高的问题:vcpkg 拉取失败(网络/代理/超时)、CMake 3.31 解压安全策略、vcpkg/ezvcpkg 的解压链路替换、以及 CMake/VS 工具集版本约束等。
如果你正在 Windows + UE 5.6 + VS2022 下编译 Cesium 插件外部依赖,这篇文章可以当作一份可复用的排查路径。
0. 环境约束:先把"不可谈判"的前提写清楚
UE 5.6 对 MSVC 的要求
UE 5.6 基本上要求 VS2022,并且常见项目会锁定到 MSVC 14.38.*(例如 14.38.33130)。如果你的机器装了多个 v143 工具集版本,必须保证:
- 生成工程、构建外部依赖、以及 UE 工程本身都使用同一套 Toolset(至少在大版本和关键 ABI 上一致)
否则后续极易出现:
- "toolset/version=14.38 不存在"的 CMake 报错
- 工具链不匹配导致的编译/链接异常
vcpkg 对 CMake 的要求
当你通过 ezvcpkg/vcpkg 构建外部依赖时,vcpkg 会根据 scripts/vcpkg-tools.json 指定的版本选择并获取工具(包括 CMake)。例如这份 vcpkg 明确写死了 cmake 3.30.1,低于该版本时会尝试下载工具包:
D:\ttzx\.ezvcpkg\2025.09.17\scripts\vcpkg-tools.json里的cmake条目(示例路径)
这就是为什么把 CMake 降到 3.23.x 往往治不了根因,反而可能触发 vcpkg 下载自己的 CMake,增加变量。
1. 问题一:vcpkg 拉不下来(GitHub/代理/12002/SSH)
Cesium 的 extern 构建链路里,vcpkg 是最重要的前置条件之一。如果 vcpkg 仓库本体都拉不下来,后面解压、编译都无从谈起。
1.1 典型报错长什么样
常见的失败形态包括:
- Git clone/checkout 失败(尤其是走
git@github.com:...的 SSH 拉取) WinHttpSendRequest failed with exit code 12002. 操作超时Downloading https://github.com/...长时间卡住后失败
1.2 根因与处理方向
这类问题本质是网络可达性/代理配置问题。优先确认:
- GitHub 是否可稳定访问(尤其是 Releases/zip 下载)
- 系统代理/全局代理是否生效(必要时设置
HTTP_PROXY/HTTPS_PROXY) - Git 是否走了正确的远端地址(SSH 或 HTTPS),以及对应的认证是否可用
1.3 实用兜底:手动下载 vcpkg(避免 GitHub/SSH 不稳定)
如果你所在网络环境对 GitHub/SSH 不稳定,最实用的方式是先把 vcpkg 的"仓库本体"以 zip 方式落地,再继续跑后续流程。
提供了一个"手动下载 vcpkg 指南"脚本:
Plugins/cesium-unreal/script-Q/download-vcpkg-manual.ps1
建议先把这一关单独跑通(确认 vcpkg 目录已存在且完整)再进入下一关。
2. 问题二:CMake 3.31 解压失败(KTX-Software 4.3.2 / Invalid empty pathname)
2.1 典型报错长什么样
外部依赖在解压阶段崩掉,日志里会出现类似:
.../tests/srcimages/テクスチャ.png: Invalid empty pathname- 或者是
cmake -E tar/tar.exe解压失败
这类问题的特点是:
- 失败发生在
-- Extracting source ... - vcpkg/下载已经过了,但在"解压源码包"阶段崩掉
2.2 根因分析
这不是 "KTX 代码有问题",而是 "解压链路"出了问题:
- vcpkg 的解压通常走 CMake 脚本内部
cmake -E tar ... - CMake 3.31 在 Windows 上对某些"路径名/编码不合规"的归档条目更严格
- 老旧压缩包(例如 KTX-Software 4.3.2)包含少量非 ASCII 文件名(例如日文),并集中在 tests 资源目录
- 结果:解压器拒绝写入某些路径名,导致整个 port 失败
2.3 稳定方案:改 ezvcpkg,让 vcpkg 强制使用"系统解压包装器"
核心思路不是"只修 KTX",而是把 vcpkg 的解压入口统一替换为我们可控的解压器。
我实际做的改动就三件事(都在同一个入口里完成):
- 在 ezvcpkg 里加了一个宏 :
EZVCPKG_PATCH_VCPKG_EXTRACTION文件:Plugins/cesium-unreal/extern/cesium-native/cmake/ezvcpkg/ezvcpkg.cmake - 每次 vcpkg 初始化后都写入一个 PowerShell 解压脚本 :
ezvcpkg-tar.ps1(写到 vcpkg 目录根下) 目的:避免 vcpkg 走cmake -E tar,同时保证脚本更新不会被缓存"卡住"。 - 扫描并替换 vcpkg 的解压命令 :把
cmake -E tar ...相关调用替换成调用ezvcpkg-tar.ps1覆盖写法:${CMAKE_COMMAND} -E tar/"${CMAKE_COMMAND}" -E tar/cmake -E tar。
ezvcpkg-tar.ps1 的解压策略(按压缩包后缀判断):
.zip:先用 .NET (System.IO.Compression.ZipFile) 解压;失败则继续走 tar 兜底.tar.gz/.tgz/.tar.*:用 Pythontarfile解压,并跳过tests/srcimages、tests/testimages(KTX 的雷点就在这里)- 其它:
tar -xf兜底,同时带--exclude跳过上述 tests 目录,并把错误输出回传到 vcpkg 日志
2.3.1 补丁代码(EZVCPKG_PATCH_VCPKG_EXTRACTION)
下面这段代码来自:
Plugins/cesium-unreal/extern/cesium-native/cmake/ezvcpkg/ezvcpkg.cmake
核心点是:生成 ezvcpkg-tar.ps1,并扫描替换 vcpkg scripts 内的 cmake -E tar 调用。
cmake
macro(EZVCPKG_PATCH_VCPKG_EXTRACTION)
if (NOT CMAKE_HOST_WIN32)
return()
endif()
set(EZVCPKG_TAR_WRAPPER "${EZVCPKG_DIR}/ezvcpkg-tar.ps1")
file(WRITE "${EZVCPKG_TAR_WRAPPER}" [=[
param(
[Parameter(Mandatory = $true)][string]$Mode,
[Parameter(Mandatory = $true)][string]$Archive
)
$ErrorActionPreference = "Stop"
$ArchivePath = (Resolve-Path -LiteralPath $Archive).Path
$Destination = (Get-Location).Path
if ($ArchivePath.ToLowerInvariant().EndsWith(".zip"))
{
try
{
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Zip = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath)
try
{
foreach ($Entry in $Zip.Entries)
{
$TargetPath = Join-Path $Destination $Entry.FullName
if ([string]::IsNullOrEmpty($Entry.Name))
{
[System.IO.Directory]::CreateDirectory($TargetPath) | Out-Null
continue
}
$TargetDir = [System.IO.Path]::GetDirectoryName($TargetPath)
if (-not [string]::IsNullOrEmpty($TargetDir))
{
[System.IO.Directory]::CreateDirectory($TargetDir) | Out-Null
}
if ([System.IO.File]::Exists($TargetPath))
{
[System.IO.File]::Delete($TargetPath)
}
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($Entry, $TargetPath)
}
}
finally
{
$Zip.Dispose()
}
exit 0
}
catch
{
}
}
$ArchiveLower = $ArchivePath.ToLowerInvariant()
if ($ArchiveLower.EndsWith(".tar.gz") -or $ArchiveLower.EndsWith(".tgz") -or $ArchiveLower.EndsWith(".tar") -or $ArchiveLower.EndsWith(".tar.bz2") -or $ArchiveLower.EndsWith(".tbz2") -or $ArchiveLower.EndsWith(".tar.xz") -or $ArchiveLower.EndsWith(".txz"))
{
$PyTemplate = @'
import tarfile
archive_path = r'{0}'
dest_dir = r'{1}'
def should_skip(name: str) -> bool:
n = name.replace('\\\\', '/').lower()
return '/tests/srcimages/' in n or '/tests/testimages/' in n
with tarfile.open(archive_path, 'r:*') as tf:
members = [m for m in tf.getmembers() if not should_skip(m.name)]
tf.extractall(dest_dir, members=members)
'@
$ArchiveEscaped = $ArchivePath.Replace(\"\\\", \"\\\\\")
$DestEscaped = $Destination.Replace(\"\\\", \"\\\\\")
$Py = $PyTemplate -f $ArchiveEscaped, $DestEscaped
$PreviousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = \"Continue\"
& py -3 -c $Py
if ($LASTEXITCODE -eq 0)
{
$ErrorActionPreference = $PreviousErrorActionPreference
exit 0
}
& python -c $Py
$ExitCode = $LASTEXITCODE
$ErrorActionPreference = $PreviousErrorActionPreference
exit $ExitCode
}
$PreviousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = \"Continue\"
$TarOutput = & tar --exclude=\"*/tests/srcimages/*\" --exclude=\"*/tests/testimages/*\" -xf $ArchivePath 2>&1
$TarExitCode = $LASTEXITCODE
if ($TarExitCode -ne 0)
{
$TarOutput | ForEach-Object { Write-Output $_ }
}
$ErrorActionPreference = $PreviousErrorActionPreference
exit $TarExitCode
]=])
set(_ezvcpkg_script_roots
\"${EZVCPKG_DIR}/scripts\"
\"${EZVCPKG_DIR}/scripts/cmake\"
)
foreach(_ezvcpkg_script_root IN LISTS _ezvcpkg_script_roots)
if (EXISTS \"${_ezvcpkg_script_root}\")
file(GLOB_RECURSE _ezvcpkg_cmake_files \"${_ezvcpkg_script_root}/*.cmake\")
foreach(_ezvcpkg_cmake_file IN LISTS _ezvcpkg_cmake_files)
file(READ \"${_ezvcpkg_cmake_file}\" _ezvcpkg_cmake_content)
if (_ezvcpkg_cmake_content MATCHES \"ezvcpkg-tar\\\\.ps1\")
continue()
endif()
set(_ezvcpkg_original_content \"${_ezvcpkg_cmake_content}\")
string(REPLACE \"COMMAND \\${CMAKE_COMMAND} -E tar\" \"COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File \\\"${EZVCPKG_TAR_WRAPPER}\\\"\" _ezvcpkg_cmake_content \"${_ezvcpkg_cmake_content}\")
string(REPLACE \"COMMAND \\\"\\${CMAKE_COMMAND}\\\" -E tar\" \"COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File \\\"${EZVCPKG_TAR_WRAPPER}\\\"\" _ezvcpkg_cmake_content \"${_ezvcpkg_cmake_content}\")
string(REPLACE \"COMMAND cmake -E tar\" \"COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File \\\"${EZVCPKG_TAR_WRAPPER}\\\"\" _ezvcpkg_cmake_content \"${_ezvcpkg_cmake_content}\")
if (NOT _ezvcpkg_cmake_content STREQUAL _ezvcpkg_original_content)
file(WRITE \"${_ezvcpkg_cmake_file}\" \"${_ezvcpkg_cmake_content}\")
endif()
endforeach()
endif()
endforeach()
endmacro()
以及在 EZVCPKG_BOOTSTRAP 里调用:
cmake
EZVCPKG_PATCH_VCPKG_EXTRACTION()
2.4 为什么要"过滤 tests 目录"
以 KTX 为例,触发问题的文件集中在:
tests/srcimages/*tests/testimages/*
这些目录通常不参与库本体构建。过滤它们可以:
- 避免 Windows 解压器在这些文件上炸掉
- 不影响真正参与编译的源码
2.5 如何验证修复是否生效(非常重要)
看 vcpkg 解压失败日志里的这一行:
Command failed: ...
如果你看到:
Command failed: powershell ... -File .../ezvcpkg-tar.ps1 ...
说明替换生效,vcpkg 已经不走 cmake -E tar。
如果你还看到:
Command failed: cmake -E tar ...
说明替换没生效(常见原因:缓存目录未刷新,或替换扫描范围不足)。
2.6 KTX 的 SHA512 会自动变化吗?
不会。vcpkg_from_github(...) 里的 SHA512 是对"下载到的源码归档(zip/tarball)字节内容"的校验值:
- vcpkg 不会自动更新;校验不通过会直接失败。
- 只有"归档内容变了"才需要更新,例如:改了
REF/URL、上游重打 tag、或你本地对归档做了二次处理并替换了原文件。
3. 问题三:CMake 降级(3.23.x)会引出 Toolset/version 问题
3.1 典型报错
当使用较老 CMake(例如 3.23.5)并指定:
-T "version=14.38"
可能会出现:
given toolset and version specification v143,version=14.38 does not seem to be installed at ...Microsoft.VCToolsVersion.14.38.props
这类问题的本质是:老 CMake 对"toolset version overlay"的识别方式与当前 VS/Toolset 安装结构不一致。
3.2 经验结论
- "降级 CMake"并不能解决"CMake 3.31 解压安全策略"这一类问题的根因
- 并且会增加一个变量:vcpkg 可能因为版本不够而下载自己的 CMake(见第 0 节)
所以更推荐:
- 保持 CMake 3.31
- 修正 vcpkg/ezvcpkg 的解压入口(第 2 节方案)
4. 建议排查顺序
建议按这个顺序推进,能最大化减少交叉干扰:
- 锁定编译环境
- MSVC 14.38
- CMake 3.31(不建议降级)
- 先打通 vcpkg 获取(仓库/下载)
- vcpkg 拉不下来先不要继续往下排
- 必要时使用
Plugins/cesium-unreal/script-Q/download-vcpkg-manual.ps1先把仓库本体落地
- 再打通 vcpkg/ezvcpkg 解压链路
- 把
cmake -E tar替换为 PowerShell 包装器 - 对 tar.* 使用 Python
tarfile并过滤 KTX tests 资源
- 把
5. 常见问题速查表
Q1:看到 Invalid empty pathname 就一定是 KTX 吗?
不一定,但绝大概率是"解压链路 + Windows 路径名规则/编码"问题。先去看失败发生在 Extracting source 还是 Downloading。
Q2:为什么我用 CMake 3.23 能跑过一次,后来又不行?
很多时候是缓存(下载/解压)导致的"偶然成功",并不代表根因解决。尤其当 vcpkg 需要下载工具或包时,网络可达性会导致结果不稳定。
结语
这套问题定位成本高的原因在于:它通常不是单点 bug,而是一组工具链边界条件的叠加:
- CMake 3.31 的行为变化
- vcpkg 的解压实现与 Windows 路径名/编码的冲突
- 网络环境导致 vcpkg 工具/源码包获取不稳定
- UE 对 Toolset 的强约束