跳转至

Linux glibc 兼容性

Orca 的 Linux 构建目标为原生 Ubuntu 20.04 及更高版本——glibc 2.31 和 libstdc++ GLIBCXX_3.4.28(也适用于 Debian 11、RHEL 9),同时支持 x64 和 arm64。 打包流程会自动强制该最低版本;添加或升级原生依赖时需牢记这一点。(可选的语音功能是唯一 例外——见下文。)

本地打包构建前置条件

pnpm run build:linux 会生成 AppImage、deb 和 RPM 产物。RPM 目标要求 PATH 中存在 rpmbuild;在 Ubuntu/Debian 上安装 rpm,在 Fedora/RHEL 上安装 rpm-build,或在 macOS 上通过 Homebrew 安装 rpm,然后在打包前使用 rpmbuild --version 验证。跨主机构建也有相同要求。

为什么需要关注

原生模块(.node)链接到编译它的那台机器上的 glibc。我们的发布 CI 在 GitHub 的 ubuntu-latest runner 上从源码编译 node-pty,随着镜像升级,其 glibc 版本会随时间升高。 在新版 glibc 上编译的二进制文件可能引用旧目标系统上不存在的符号版本,动态加载器随后会拒绝加载它:

/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by .../pty.node)

由于 Orca 主进程在启动时加载 node-pty,该失败会在窗口出现前导致整个应用崩溃——这正是 v1.4.150 中发布的问题,并导致 Ubuntu 20.04 上启动失败(#9902)。

具体陷阱是 glibc 2.32–2.34 的“libpthread/libutil 合并”,它将多个长期稳定的函数移入 libc,并使用了全新的符号版本:

符号 新版本 node-pty 用途
pthread_sigmask GLIBC_2.32 重置子进程信号掩码
openpty GLIBC_2.34 分配 pty
forkpty GLIBC_2.34 fork shell

Electron 本身(glibc 2.25)以及其他捆绑的原生模块(sherpa-onnx、@parcel/watcher, 两者均在旧 glibc 上预构建)远低于最低版本,因此 node-pty 是唯一的阻塞项。

如何守住最低版本

1. 固定被迁移的符号(修复方案)。 config/patches/node-pty@1.1.0.patch 在 src/unix/pty.cc 中添加一个 .symver 垫片,将 openpty、forkpty 和 pthread_sigmask 绑定到合并前的版本节点——x64 上为 GLIBC_2.2.5, arm64 上为 GLIBC_2.17(各架构的基线 glibc)。glibc 仍然将这些作为兼容别名提供, 因此该引用既能在新的构建主机上解析,也能在旧目标系统上解析。

问题在于:gcc 默认使用 --as-needed,并且由于被固定的符号在构建时从 libc 的兼容别名解析, 它会将 libutil/libpthread 从 DT_NEEDED 中移除。在目标系统上,这些库才是符号实际所在之处, 因此补丁的 binding.gyp ldflags 会强制将 -Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0 重新加入 DT_NEEDED。 该垫片由 #if defined(__linux__) 保护;macOS 和 Windows 不受影响。

2. 对打包进行门禁(回归防护)。 config/scripts/verify-linux-glibc-floor.cjs 在 Linux 的 electron-builder afterPack 钩子中运行。它读取每个捆绑原生二进制的版本需求 (objdump -p 的 "Version References"——权威的加载时列表,也会捕获无符号标记,例如 GLIBC_ABI_DT_RELR),如果任何强 GLIBC_/GLIBCXX_/ CXXABI_ 版本节点比原生 Ubuntu 20.04 提供的版本更新,则使构建失败,并指明文件和出错的版本节点。 弱需求会被忽略(加载器可以容忍它们)。它还断言 .symver 修复的另一面:任何导入 openpty/forkpty 的二进制文件都必须将 libutil.so.1 保留在 DT_NEEDED 中——否则, 被固定的 openpty@GLIBC_2.2.5 会在构建时从 libc 的兼容别名解析(因此版本检查会通过), 但在 20.04 上加载失败,因为那些函数只存在于 libutil 中。因此,未来 runner 升级、新的原生依赖 或遗漏的 ldflag 都会导致发布构建失败,而不是发布一个启动即崩溃的 Linux 应用。

该门禁是一个静态不变量,而不是集成测试。本次修复的加载路径已手动验证(真实 Ubuntu 20.04, x64 + arm64:require node-pty 并派生一个 shell)。建议在 CI 中添加一个冒烟测试, 在 glibc-2.31 容器中加载打包后的 pty.node 并派生一个 shell——这将使加载路径可自我验证, 即使构建未来迁移到旧 glibc sysroot 也仍然有效。

唯一的例外是 sherpa-onnx 语音预构建,它已经需要 GLIBCXX_3.4.29(GCC 11)。 它在语音 worker(src/main/speech/stt-worker.ts)中延迟加载,绝不在应用启动时加载, 因此不受 libstdc++ 最低版本限制——但其 glibc 需求仍会被检查。因此,语音转文本需要一个 具有 GCC 11+ libstdc++ 的主机(Ubuntu 21.10 / 22.04 LTS 或更高版本);应用本身仍可在 原生 20.04 上启动。

3. 在加载前检查,适用于未附带编译器的主机(orcad)。 上述两个门禁保护的是打包后的桌面应用,其中二进制文件由同一流水线构建并验证。orcad 部署到 Orca 从未在其上构建的主机,因此它增加了一个运行时前置条件 (src/main/orcad/node-pty-precondition.ts), 在 main.ts 中于任何内容 require node-pty 之前运行。它在子进程中加载该 addon, 因此加载器拒绝的二进制文件——或完全中止的二进制文件——只是数据,而不是本进程的死亡, 运维人员会得到一句话,指明主机的 libc、其 Node ABI、其预构建槽位以及要运行的命令。 一个被证明无法加载的二进制文件会以 78(EX_CONFIG)退出,而不是到达 require; 一个从未响应的探测会被报告为无法验证,并且仍会启动,因为沉默的探测不是证据。 它发现的任何内容都会发布在 status.get 的 degradations[] 下的 terminal_unavailable 中。

4. 发布从打过补丁的源码构建的二进制文件。 config/scripts/build-orcad-prebuilds.mjs (pnpm run build:orcad-prebuilds,在 build:orcad 之后)会为当前主机编译 node-pty,并将其放入 out/orcad/prebuilds/<slot>/,其中 slot 是 linux-{x64,arm64}-{glibc,musl} 或 darwin-{x64,arm64}。libc 是 slot 名称的一部分, 因为 node-pty 自身的加载器会回退到 prebuilds/<platform>-<arch>,并且无法区分 glibc 和 musl —— 放在那里的 glibc 二进制文件会在 Alpine 上被加载,并在 dlopen 时崩溃。 该脚本拒绝编译未应用 config/patches/node-pty@1.1.0.patch 的源码树:没有该补丁,预构建产物会成为作为交付物发布的 #9902 崩溃, 而不是首次连接错误。CI 会在匹配的容器内为每个 slot 运行一次 (--slot= 强制指定标签),合并各源码树,并且如果矩阵中存在缺口,--require-slots 会使发布失败。

添加或升级原生依赖

  • 优先选择提供预构建二进制文件的包,这些文件针对旧工具链编译 (manylinux / glibc 2.17 级别),例如 @parcel/watcher。
  • 对于我们从源码编译的模块,如果门禁标记了它,请像 node-pty 那样固定有问题的符号,或在旧 glibc 容器中构建它。
  • 要在 Linux 主机上本地检查,请列出二进制文件所依赖的内容(跳过加载器可容忍的带 0x02 标记的弱依赖):
objdump -p path/to/module.node | sed -n '/Version References/,/^$/p'

任何强 GLIBC_ 节点都不得超过 2.31,任何 GLIBCXX_/CXXABI_ 节点都不得超过 3.4.28/1.3.12 —— 即标准 Ubuntu 20.04 自带的版本。

运行时下限:glibc 2.41 以下的 environ 竞态(Electron ≥ 43.7.0)

与上述构建下限不同,一个 glibc 运行时缺陷限制了我们可发布的 Electron 版本。在 glibc 2.41 之前,setenv/unsetenv 会重新分配 environ 数组并 free 旧数组,因此另一线程上并发的 getenv() 会读取已释放的内存。Ubuntu 20.04–24.04(2.31–2.39)都低于该版本线, 因此我们支持的每个 Linux 目标都受影响。

Electron 43.5.0 使该潜在竞态在每次启动时都可触发:它开始在 gtk_init() 前后设置 GDK_GL=disable,并在之后立即取消设置, 同时在同一变更中将 FontConfig 预热移到线程池线程上,该线程并发运行并不断调用 getenv() (electron#53070)。结果是浏览器进程在启动约一秒后发生 use-after-free —— 没有窗口, 也不涉及 GPU 子进程,内存损坏会在下一次分配落地的任何位置显现,这就是为什么报告中会出现不相关的帧(gtk_widget_realize、 libxcb-dri3、FontConfig/expat)。Orca 1.4.199/1.4.200 发布了该运行时,并在 Ubuntu + NVIDIA/X11 上启动时崩溃 (#20081)。

Electron 43.7.0 通过重写 setenv/unsetenv/putenv/clearenv 来修复该问题,使已发布的 environ 永远不会被释放, 在 2.41+ 上则交由 glibc 处理(electron#53491,已回移到 42/43/44/45)。 在确认目标发布版本包含该回移之前,请勿将 Electron 降级到 43.7.0 以下,或切换到其他版本线 —— 如果固定版本低于下限,config/scripts/electron-runtime-floor.test.ts 会使测试套件失败。Orca 本身在早期启动期间会写入 process.env(patchPackagedProcessPath、configureOrcaUserDataPathEnv、 hydrate-shell-path),因此它是一等触发因素,而不仅仅是旁观者。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca