likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 9

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

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

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安装Electron的坑,你遇到了吗?

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

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

├── 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安装Electron的坑,你遇到了吗?

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

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

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

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

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

"scripts": {
	"prepare": "rimraf node_modules/electron/dist && node node_modules/electron/install.js"
}

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

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修改为:

{
  "prepare": "node build/install.cjs"
}

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

进展

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

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

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

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

排在第一位,哦,是pnpm之父,失敬: pnpm安装Electron的坑,你遇到了吗?

另一个方案

言归正传。

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

我试了下,修改.npmrc

side-effects-cache = false

果然生效。

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

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

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

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

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

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

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

Bug修复解析

紧接着,这个Bug就被大佬修复了: pnpm安装Electron的坑,你遇到了吗?

翻译过来是:

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

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

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

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

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

我们使用corepack切换pnpmv8.15.4

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的修复逻辑

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

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

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

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

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

mkdir test
cd test
ln -s  ../node_modules aa

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

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

再写段JS代码:

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。

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

总结

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