pnpm安装Electron的坑,你遇到了吗?

MacOS系统里,使用pnpm安装Electron后,正常使用没有问题。但是,如果删除了node_modules,重新安装后,启动应用时会报错:

bash 复制代码
jw my-electron-app % pnpm start

> my-electron-app@1.0.0 start /Users/jw/wk/github/electron/my-electron-app
> electron .

dyld[38219]: Library not loaded: @rpath/Electron Framework.framework/Electron Framework
  Referenced from: <4C4C4421-5555-3144-A154-D5199CCDD1BE> /Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
  Reason: tried: '/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/Library/Frameworks/Electron Framework.framework/Electron Framework' (no such file), '/System/Library/Frameworks/Electron Framework.framework/Electron Framework' (no such file, not in dyld cache)
/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron exited with signal SIGABRT
 ELIFECYCLE  Command failed with exit code 1.

排查

我们进入到报错的文件的上级目录,看到确实是没有这个文件Electron Framework,只有一个文件夹Versions

bash 复制代码
jw my-electron-app % cd "/Users/jw/wk/github/electron/my-electron-app/node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/"
jw Electron Framework.framework % ls
Versions

从错误提示上看,应该是Electron Framework这个文件在安装时没有复制过来导致的,使用npmyarn安装并不会出现这个问题,锅显然是pnpm的。

我们知道,pnpm有别于npmyarn的核心卖点是,使用硬链接和符号链接的方式来管理node_modules目录,这种方式可以避免重复下载和存储相同的包,从而节省磁盘空间和提高安装速度。这个错误信息必然与这种链接方式有关。

我们在正常的版本中找到这个目录node_modules/.pnpm/electron@27.0.4/node_modules/electron/dist/Electron.app,看下里面有什么内容:

bash 复制代码
├── Electron Framework
├── Helpers
│   └── chrome_crashpad_handler
├── Libraries
│   ├── libEGL.dylib
│   ├── libGLESv2.dylib
│   ├── libffmpeg.dylib
│   ├── libvk_swiftshader.dylib
│   └── vk_swiftshader_icd.json
├── Resources
└── Versions

177 directories, 207 files

MacOS上看,Electron Framework是个可执行文件:

遇事不决问谷歌,很快找到了这条有关联的信息,是pnpm的GitHub的issue

这个问题在2023年10月就已经被提出了,顺便还给了一个临时解决方案:

也就是在preparehook里,先删除这个目录,再重新执行一次Electroninstall脚本:

json 复制代码
"scripts": {
	"prepare": "rimraf node_modules/electron/dist && node node_modules/electron/install.js"
}

由于只有Mac有这个问题,所以我写了一个简单的脚本install.cjs

javascript 复制代码
const os = require("os");
const fs = require("fs");

function main() {
  if (os.platform() !== "darwin") {
    return;
  }
  console.info("rm -rf node_modules/electron/dist");
  fs.rmSync("node_modules/electron/dist", { force: true, recursive: true });
  console.info("node node_modules/electron/install.js");
  require("electron/install.js");
}

main();

将上述script修改为:

json 复制代码
{
  "prepare": "node build/install.cjs"
}

以上是我在2024年1月发现这个问题的处理。

进展

时间进展到4月,我发现这个issue被关闭了:

这位大佬Zoltan Kochan是谁呢?到pnpm的贡献榜里一看:

排在第一位,哦,是pnpm之父,失敬:

另一个方案

言归正传。

大佬在2月24日提出:『我认为这个问题的临时解决方法是将side-effects-cache设置为 false』。

我试了下,修改.npmrc

bash 复制代码
side-effects-cache = false

果然生效。

这个配置是什么呢?它是pnpm的特有的配置项,在官方文档里是这样写的:

也就是说,side-effects-cache用于配置是否缓存包的安装副作用。

当你使用pnpm安装一个包时,pnpm会在node_modules目录下创建一个符号链接,指向一个全局的包存储位置。这样,如果多个项目使用同一个包,那么这个包只需要在全局存储位置存储一次,而不是在每个项目的node_modules目录下都存储一份。

然而,有些包在安装过程中会有副作用,比如编译本地代码或者生成某些文件。这些副作用通常是项目特定的,不能在多个项目之间共享。

这就是side-effects-cache配置项的作用。如果设置为truepnpm会在每个项目的node_modules目录下缓存这些副作用。这样,即使这个包在全局存储位置已经存在,pnpm也会重新执行这个包的安装过程,以生成这些副作用。

如果设置为falsepnpm则不会缓存这些副作用。这意味着,如果一个包在全局存储位置已经存在,pnpm就不会重新执行这个包的安装过程,即使这个过程可能会生成一些项目特定的副作用。

Bug修复解析

紧接着,这个Bug就被大佬修复了:

翻译过来是:

符号链接被解析为其真实路径并上传为真实文件。它们不会占用更多空间,因为硬链接最终指向同一文件。这可能令人惊讶,因为在首次安装时,用户会看到符号链接,但在后续安装中,软件包将使用"真实文件",因为它们将被链接到存储库(来自副作用缓存)。我认为理想情况下,在从副作用缓存链接软件包时,我们应该恢复符号链接,但这将需要对软件包索引文件进行破坏性更改,因为目前仅支持"真实文件"条目。

已经合并到main分支和v9 beta版本上了:

再看发版日志里,在v8.15.4里果然有了这条信息:

我们使用corepack切换pnpmv8.15.4

bash 复制代码
jw my-electron-app % corepack use pnpm@8.15.4
Installing pnpm@8.15.4 in the project...

Lockfile is up to date, resolution step is skipped
Packages: +78
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 78, reused 78, downloaded 0, added 78, done

devDependencies:
+ electron 27.0.4

Done in 540ms

这时发现pnpm start果然正常了。

下来,我们简单看下这个Bug的修复逻辑

大佬只是将代码顺序调整了一下,额外添加了一句stat.isDirectory的处理。

你可能会产生疑惑,什么情况下,在33fs.readdir判断过不是文件夹,到了47行又使用fs.stat判断是个文件夹的呢?

答案是pnpm的核心------符号链接。

我们做个测试,方便理解。随便找一个工程,使用ln -s创建一个文件夹的符号链接:

bash 复制代码
mkdir test
cd test
ln -s  ../node_modules aa

在IDE里能看到创建成功了:

再写段JS代码:

javascript 复制代码
const fs = require("fs");
const path = require("path");

const dir = "./test";
fs.readdirSync(dir, {
  withFileTypes: true,
}).forEach((file) => {
  console.log(
    "file:",
    file.name,
    "isDir:",
    file.isDirectory(),
    "isSymLink:",
    file.isSymbolicLink()
  );
  const fullPath = path.join(dir, file.name);
  let stat;
  try {
    stat = fs.statSync(fullPath);
    console.log("isDir:", stat.isDirectory());
  } catch (err) {
    if (err.code !== "ENOENT") {
      throw err;
    }
  }
});

// file: aa isDir: false isSymLink: true
// isDir: true

从打印结果就能看出,在fs.readdir里,如果返回的这个Dirent对象表示的是一个指向目录的符号链接,isDirectory方法会返回false,因为符号链接本身并不是目录。而fs.stat会"解引用"符号链接,也就是说,它会返回符号链接指向的目录或文件的状态,而不是符号链接本身的状态。

大佬的这次修改,考虑了文件夹为符号链接的情况,又考虑到file为符号链接(file.isSymbolicLink,原来判断是file.isFile)的情况,完美解决了这个Bug。

总结

MacOS中使用pnpm二次安装Electron后,可能会出现node_modules里缺失了二进制文件的情况,这是pnpm符号链接引发的一个Bug,不确定是具体哪个版本出的问题(8.0.0是好的),所以请大家升级到最新版本(v8.15.4以上),或者v9 beta版本。

相关推荐
AlienZHOU14 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc17 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师18 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen19 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒19 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow19 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81819 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007819 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明19 小时前
第 6 章 组件进阶
前端·vue.js
江华森19 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端