一次 node_modules 安装失败的排查之旅

April 28, 2020

前言

前段时间,我更换了博客的主题,博客是使用 Gatsby 搭建的,它基于 React ,利用 Markdown 搭配 GraphQL,可帮助开发者快速构建博客站点。

但在我安装依赖包时却掉入了天坑,此文用于记录我的脱坑过程。

查看源码

进入项目,第一步肯定是安装 node_modules 包,在我执行了 yarn install 后,终端输出了以下错误:

这里的 Output 指出:在安装 pngquant-bin 依赖包(一个针对 PNG 格式图像的压缩库)时,意外发生了 3 个错误。

于是我进入到 node_moduls/pngquant-bin 目录下,企图查看源码来一探究竟。

我首先查看了 package.json 文件,它涵盖了许多非常有用的信息,如:

  • 依赖包的入口执行文件,被 main 所定义,默认为同级目录下的 index.js
  • 依赖包有哪些 scripts 脚本

我观察到,它没有指明 main 字段,即入口文件是 index.js,但 scripts 中定义了 postinstall.

json
{
"scripts": {
"postinstall": "node lib/install.js",
"test": "xo && ava"
}
}
json
{
"scripts": {
"postinstall": "node lib/install.js",
"test": "xo && ava"
}
}

postinstall 相当于一个钩子函数,会在恰当的时机执行,类似于 React 生命周期中的 componentDidMount,比如我定义了以下 scripts:

json
{
"scripts": {
"preinstall": "echo I run before the install script",
"postinstall": "echo I run after the install script"
}
}
json
{
"scripts": {
"preinstall": "echo I run before the install script",
"postinstall": "echo I run after the install script"
}
}

当我在命令行中输入 yarn install,会自动按照下面的顺序执行,NPM 也是如此。

bash
$ yarn run preinstall && yarn install && yarn run postinstall
bash
$ yarn run preinstall && yarn install && yarn run postinstall

输出:

bash
echo I run before the install script
// installing modules...
echo I run after the install script
bash
echo I run before the install script
// installing modules...
echo I run after the install script

有了这条关键线索,我们直接进入到 node_moduls/pngquant-bin/lib/install.js 目录下,看看当 pngquant-bin 被成功安装后,又做了哪些步骤?

js
"use strict";
const path = require("path");
const binBuild = require("bin-build");
const log = require("logalot");
const bin = require(".");
bin
.run(["--version"])
.then(() => {
log.success("pngquant pre-build test passed successfully");
})
.catch(err => {
log.warn(err.message);
log.warn("pngquant pre-build test failed");
log.info("compiling from source");
const libpng = process.platform === "darwin" ? "libpng" : "libpng-dev";
binBuild
.file(path.resolve(__dirname, "../vendor/source/pngquant.tar.gz"), [
"rm ./INSTALL",
`./configure --prefix="${bin.dest()}"`,
`make install BINPREFIX="${bin.dest()}"`,
])
.then(() => {
log.success("pngquant built successfully");
})
.catch(err => {
err.message = `pngquant failed to build, make sure that ${libpng} is installed`;
log.error(err.stack);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
});
js
"use strict";
const path = require("path");
const binBuild = require("bin-build");
const log = require("logalot");
const bin = require(".");
bin
.run(["--version"])
.then(() => {
log.success("pngquant pre-build test passed successfully");
})
.catch(err => {
log.warn(err.message);
log.warn("pngquant pre-build test failed");
log.info("compiling from source");
const libpng = process.platform === "darwin" ? "libpng" : "libpng-dev";
binBuild
.file(path.resolve(__dirname, "../vendor/source/pngquant.tar.gz"), [
"rm ./INSTALL",
`./configure --prefix="${bin.dest()}"`,
`make install BINPREFIX="${bin.dest()}"`,
])
.then(() => {
log.success("pngquant built successfully");
})
.catch(err => {
err.message = `pngquant failed to build, make sure that ${libpng} is installed`;
log.error(err.stack);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
});

我惊喜地发现,终端中所有错误信息都定义于此,我们只需按照代码执行逻辑逐步排查即可。

第一步执行了 bin.run(['--version']),它判断了是否存在 可执行文件(win 下为 .exe 后缀),从而进行后续操作,而依据终端的错误信息,该命令执行失败,于是:

js
log.warn("pngquant pre-build test failed");
js
log.warn("pngquant pre-build test failed");

而二进制文件 bin 来源于 require('.'),即当前目录下的 index.js,于是我来到 node_moduls/pngquant-bin/lib/index.js 中:

js
"use strict";
const path = require("path");
const BinWrapper = require("bin-wrapper");
const pkg = require("../package.json");
const url = `https://raw.githubusercontent.com/imagemin/pngquant-bin/v${pkg.version}/vendor/`;
module.exports = new BinWrapper()
.src(`${url}macos/pngquant`, "darwin")
.src(`${url}linux/x86/pngquant`, "linux", "x86")
.src(`${url}linux/x64/pngquant`, "linux", "x64")
.src(`${url}freebsd/x64/pngquant`, "freebsd", "x64")
.src(`${url}win/pngquant.exe`, "win32")
.dest(path.resolve(__dirname, "../vendor"))
.use(process.platform === "win32" ? "pngquant.exe" : "pngquant");
js
"use strict";
const path = require("path");
const BinWrapper = require("bin-wrapper");
const pkg = require("../package.json");
const url = `https://raw.githubusercontent.com/imagemin/pngquant-bin/v${pkg.version}/vendor/`;
module.exports = new BinWrapper()
.src(`${url}macos/pngquant`, "darwin")
.src(`${url}linux/x86/pngquant`, "linux", "x86")
.src(`${url}linux/x64/pngquant`, "linux", "x64")
.src(`${url}freebsd/x64/pngquant`, "freebsd", "x64")
.src(`${url}win/pngquant.exe`, "win32")
.dest(path.resolve(__dirname, "../vendor"))
.use(process.platform === "win32" ? "pngquant.exe" : "pngquant");

这段代码大意是通过 bin-wrapper 库,依据实际安装开发下的 OS,去动态下载相应的 pngquant 可执行文件(download raw file),存放于 node_moduls/pngquant-bin/vendor 目录下。

我的 OS 是 win,理应生成了 /vendor/pngquant.exe,但我的 vendor 目录下并不存在 pngquant.exe,导致 --version 命令执行失败。

网络排查

一开始我也好奇,既然可以在 github.com 直接下载 pngquant.exe 文件,而代码中定义的下载地址却是:raw.githubusercontent.com,这不是多此一举嘛。

但我的想法是错误的,即使你在 github.com 点击 download 或 View raw,实际上请求的地址是 raw.githubusercontent.com 域名上的资源。

raw.githubusercontent.com 是 github 的素材服务器 (assets server), 避免跟主服务抢占负载。

结合终端中的错误信息:getaddrinfo ENOENT raw.githubusercontent.com,很大概率是因为网络问题,毕竟外网时常被墙,但关键是我已经使用了科学上网的软件,直接访问 https://raw.githubusercontent.com 是 OK 的。

我又仔细一想,科学上网的原理是我的电脑通过正向代理服务器去访问外网,我的 yarn 虽然设置了淘宝镜像源,下载依赖包是没问题,但在 代码中主动访问国外站点,显然会失败。

经过一番搜索,我查到可以修改配置来手动设置代理,yarn 就能访问国外站点,在查看了我的代理地址后(如果你没有翻墙,请跳过,直接看下一节的解决方案),我在终端执行了以下代码:

bash
$ yarn config set proxy http://127.0.0.1:19180/
yarn config v1.22.4
success Set "proxy" to "http://127.0.0.1:19180/".
Done in 0.10s.
bash
$ yarn config set proxy http://127.0.0.1:19180/
yarn config v1.22.4
success Set "proxy" to "http://127.0.0.1:19180/".
Done in 0.10s.

重新 yarn install,安装过程犹如德芙巧克力丝滑般顺利!

回过头,来到 node_moduls/pngquant-bin/vendor 目录中,运行以下命令:

bash
.\pngquant.exe --version
2.11.7 (January 2018)
bash
.\pngquant.exe --version
2.11.7 (January 2018)

DNS 污染

还有一种说法,是 raw.githubusercontent.com 的域名解析竟然因某些你懂的原因给临时污染了,需要手动修 hosts 文件。

通过 IPAddress.com,输入 raw.githubusercontent.com 查询到真实的 IP 地址,然后在 hosts 文件的末尾写入以下内容:

bash
199.232.68.133 raw.githubusercontent.com
bash
199.232.68.133 raw.githubusercontent.com

总结

从入坑到脱坑,我花费了巨大的时间和精力。因为我一开始的切入点,是从 Error: pngquant failed to build, make sure that libpng-dev is installed 出发,惯性思维让我觉得 Error 级别的 Log 才是最关键的,导致我止步原地,一昧去关心 libpng 的相关底层环境在系统上是否存在。

这里有个小插曲,在我本机安装失败后,我尝试在购置的 Ubuntu 云服务器上安装,结果并没有出错,因此在我快要放弃这个切入点时,又相信了是 OS 缺少依赖环境的原因。

但后来我查看代码才知道,这里有个小细节,pngquant-bin 默认在 vendor/source 目录下放置了 pngquant.tar.gz,即使因为网络原因下载 pngquant 失败,也能通过源码编译成功安装。但我的电脑是 win,压根无法执行 ./configure make install 等命令,所以用 Mac 做开发,它不香嘛,因为穷……

其实,终端中打印的错误信息,都是蝴蝶效应的结果:

  1. 因为无法访问 raw.githubusercontent.com,致使打印第一个错误:‼ getaddrinfo ENOENT raw.githubusercontent.com
  2. 可执行文件无法执行,打印第二个错误:‼ pngquant pre-build test failed
  3. 程序便启用了源码编译,编译失败,打印第三个错误: × Error: pngquant failed to build, make sure that libpng-dev is installed

我把一个简单的网络问题给复杂化了,究其根本,我在初始阶段就本末倒置,将问题的排查重心搞反了。

最正确的路,是结合终端输出和源码内容进行故障排查,同时网络原因不可忽视

在此,总结经验,告诫自己。

Reference:


B2D1 (包邦东)

Written by B2D1 (包邦东)