在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
这个文件在安装时没有复制过来导致的,使用npm
和yarn
安装并不会出现这个问题,锅显然是pnpm
的。
我们知道,pnpm
有别于npm
和yarn
的核心卖点是,使用硬链接和符号链接的方式来管理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月就已经被提出了,顺便还给了一个临时解决方案:
也就是在prepare
的hook
里,先删除这个目录,再重新执行一次Electron
的install
脚本:
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
配置项的作用。如果设置为true
,pnpm
会在每个项目的node_modules
目录下缓存这些副作用。这样,即使这个包在全局存储位置已经存在,pnpm
也会重新执行这个包的安装过程,以生成这些副作用。
如果设置为false
,pnpm
则不会缓存这些副作用。这意味着,如果一个包在全局存储位置已经存在,pnpm
就不会重新执行这个包的安装过程,即使这个过程可能会生成一些项目特定的副作用。
Bug修复解析
紧接着,这个Bug就被大佬修复了:
翻译过来是:
符号链接被解析为其真实路径并上传为真实文件。它们不会占用更多空间,因为硬链接最终指向同一文件。这可能令人惊讶,因为在首次安装时,用户会看到符号链接,但在后续安装中,软件包将使用"真实文件",因为它们将被链接到存储库(来自副作用缓存)。我认为理想情况下,在从副作用缓存链接软件包时,我们应该恢复符号链接,但这将需要对软件包索引文件进行破坏性更改,因为目前仅支持"真实文件"条目。
已经合并到main分支和v9 beta
版本上了:
再看发版日志里,在v8.15.4
里果然有了这条信息:
我们使用corepack
切换pnpm
为v8.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
的处理。
你可能会产生疑惑,什么情况下,在33
行fs.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
版本。