UE5.6 Cesium 插件编译踩坑记录(UE 5.6 + MSVC 14.38 + CMake 3.31)

这是一篇"从报错到根因,再到稳定落地方案"的复盘文章,记录我在 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 的解压入口统一替换为我们可控的解压器。

我实际做的改动就三件事(都在同一个入口里完成):

  1. 在 ezvcpkg 里加了一个宏EZVCPKG_PATCH_VCPKG_EXTRACTION 文件:Plugins/cesium-unreal/extern/cesium-native/cmake/ezvcpkg/ezvcpkg.cmake
  2. 每次 vcpkg 初始化后都写入一个 PowerShell 解压脚本ezvcpkg-tar.ps1(写到 vcpkg 目录根下) 目的:避免 vcpkg 走 cmake -E tar,同时保证脚本更新不会被缓存"卡住"。
  3. 扫描并替换 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.*:用 Python tarfile 解压,并跳过 tests/srcimagestests/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. 建议排查顺序

建议按这个顺序推进,能最大化减少交叉干扰:

  1. 锁定编译环境
    • MSVC 14.38
    • CMake 3.31(不建议降级)
  2. 先打通 vcpkg 获取(仓库/下载)
    • vcpkg 拉不下来先不要继续往下排
    • 必要时使用 Plugins/cesium-unreal/script-Q/download-vcpkg-manual.ps1 先把仓库本体落地
  3. 再打通 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 的强约束

相关推荐
feng_you_ying_li3 小时前
c++之哈希表的介绍与实现
开发语言·c++·散列表
xh didida3 小时前
C++ -- string
开发语言·c++·stl·sring
m晴朗3 小时前
测试覆盖率从35%到80%:我用AI批量生成C++单元测试的完整方案
c++·gpt·ai
无限进步_3 小时前
【C++&string】大数相乘算法详解:从字符串加法到乘法实现
java·开发语言·c++·git·算法·github·visual studio
苏纪云4 小时前
蓝桥杯考前突击
c++·算法·蓝桥杯
‎ദ്ദിᵔ.˛.ᵔ₎4 小时前
模板template
开发语言·c++
charlie1145141914 小时前
通用GUI编程技术——图形渲染实战(二十九)——Direct2D架构与资源体系:GPU加速2D渲染入门
开发语言·c++·学习·架构·图形渲染·win32
小肝一下4 小时前
每日两道力扣,day8
c++·算法·leetcode·哈希算法·hot100
CheerWWW4 小时前
C++学习笔记——线程、计时器、多维数组、排序
c++·笔记·学习