一次 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-exitprocess.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-exitprocess.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.4success 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.4success Set "proxy" to "http://127.0.0.1:19180/".Done in 0.10s.
重新 yarn install,安装过程犹如德芙巧克力丝滑般顺利!
回过头,来到 node_moduls/pngquant-bin/vendor
目录中,运行以下命令:
bash
.\pngquant.exe --version2.11.7 (January 2018)
bash
.\pngquant.exe --version2.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 做开发,它不香嘛,因为穷……
其实,终端中打印的错误信息,都是蝴蝶效应的结果:
- 因为无法访问 raw.githubusercontent.com,致使打印第一个错误:‼ getaddrinfo ENOENT raw.githubusercontent.com
- 可执行文件无法执行,打印第二个错误:‼ pngquant pre-build test failed
- 程序便启用了源码编译,编译失败,打印第三个错误: × Error: pngquant failed to build, make sure that libpng-dev is installed
我把一个简单的网络问题给复杂化了,究其根本,我在初始阶段就本末倒置,将问题的排查重心搞反了。
最正确的路,是结合终端输出和源码内容进行故障排查,同时网络原因不可忽视。
在此,总结经验,告诫自己。
Reference: