Others

视频渲染一次往 .app 里写了 142 MB 缓存,macOS 应用的签名随之失效

1265 字约 5 分钟

摘要一个 Electron 桌面应用里的视频生成功能,渲染一次就会往应用包内部写入约 142 MB 的 webpack 缓存,macOS 的代码签名校验随即失败。记录如何定位到写入位置、为什么不能靠配置改位置、怎样验证修复,以及关掉缓存的代价。

我参与的那个开源 Electron 桌面应用,有一个用 Remotion 生成视频的功能。我在做 macOS 真机验收的时候发现了一个现象:用过一次视频生成之后,再去校验应用的签名,就失败了。

macOS 的 .app 是个目录,签名会把里面每个文件都封进一份清单,叫“封印”(seal)。签名之后,只要往包里增加、删除或修改任何一个文件,封印就破了,codesign --verify 会报 a sealed resource is missing or invalid。所以只要运行时有任何东西往应用包内部写文件,签名就会被破坏。

这件事之前就被标成“已知问题”放着,那天我决定把它处理掉。

先看它到底往里写了什么

助手没有靠猜,先做了一个前后对照。它拿一份正式发布的包的副本,渲染之前先记下签名校验结果和包内的完整文件清单,再用应用自带的 Node 直接调用包里的渲染模块,连续渲染两次一个合成视频,渲染之后再对比。

渲染前签名校验通过,渲染后失败。包内多出了 7 个路径,全部在 node_modules/.cache/webpack/ 下面,加起来约 142 MB。

为什么会写在那里

Remotion 打包视频模板时,用的是 webpack,webpack 默认会把文件系统缓存写在项目根目录的 node_modules/.cache/webpack 里。而 Remotion 认为的“项目根”,是入口文件最近的那个 package.json 所在的目录,打包时还会切换到那个目录。

桌面版里,视频模板是随应用一起装进包里的,离它最近的 package.json 属于应用包内部的一个共享服务包。所以缓存就落在了应用包里面。

最自然的想法是让缓存换个位置,但这条路走不通:Remotion 是在应用 webpackOverride 之后才去设置 cache 的,我们在覆盖配置里改什么都会被它盖掉。能做的只有一件事,让它别缓存。

修复:只对包内的固定模板关缓存

改动很小:

  • 渲染前准备合成的函数多了一个 enableCaching 参数,默认是 true,原样传给 Remotion 的 bundle()。
  • 打包内置模板时传 false。用户自己工作区里可以编辑的视频项目,仍然照常缓存,它们的项目根在用户工作区里,缓存也落在那里,不在应用包内。
  • 补了一条回归测试,同时锁定两种行为:内置模板不缓存,默认调用仍然缓存。去掉修复再跑,结果会变成 [true, true],测试会失败,说明它能抓住回退。

验证

测试和类型检查通过之外,我更关心的是真的能把签名保住。助手用同一套对照方法,拿两个同样的正式包副本,一个原样,一个把改过的两个文件替换进去并用本机测试身份重新签名,分别连续渲染两次。

原包副本修复后的副本
渲染后包内新增的路径7 个,约 142 MB0
渲染后签名校验失败通过
打包阶段耗时,第 1 次 / 第 2 次5.4 秒 / 1.4 秒4.5 秒 / 3.4 秒

两组渲染出来的视频帧数、时长、分辨率都一样,都是 90 帧、3.05 秒。助手又抽了一帧画面比对,完全一致,排除了关掉缓存让第一次渲染少了内容(比如字体没加载)的可能。

代价

关掉缓存,第二次起每次渲染的打包阶段多约 2 秒(1.4 秒变成 3.4 秒)。第一次渲染反而略快,因为不用再把缓存写盘。对一个渲染本身要几十秒的功能来说,这个代价可以接受。

没测到的

  • Windows 上没有实机渲染。同一段代码路径也会把缓存写进安装目录,修复方式相同,但没有验证。
  • 没有通过桌面界面走完整的视频生成流程,验证是直接调用包内渲染模块完成的。
  • 验证用的是已发布的上一个版本的包,没有等新版本。因为 GitHub 的下载速度只有每秒 50 到 110 KB,一个包要好几个小时。助手先逐字节核对过,包内相关的三个文件和当前主干完全相同,使用的 Remotion 版本也一样,才用它做代表。
通用的排查办法

怀疑运行时在往签名后的 .app 里写东西,不需要读代码,对比运行前后的文件清单就够了,再配合 codesign --verify --strict --verbose=2 看签名是否还有效。这一招同样适合排查日志、缓存、自动生成的配置文件。缓存和日志应该写到用户数据目录,不要写进应用包。

测试里还有几项在 macOS 上本来就失败,比如路径比较时 /var 实际指向 /private/var,助手撤下改动前后各跑了一遍,失败集合完全相同,说明这些失败和这次改动无关。

评论