node-gyp
是一个用 Node.js 编写的跨平台命令行工具,用于为 Node.js 编译本机插件模块。它包含之前由 Chromium 团队使用的 gyp-next项目的供应副本,扩展以支持 Node.js 原生插件的开发。
node-gyp
is a cross-platform command-line tool written in Node.js for compiling native addon modules for Node.js. It contains a vendored copy of the gyp-next project that was previously used by the Chromium team, extended to support the development of Node.js native addons.
node是跨平台的,那么对于任何的node模块理论也是应该是跨平台的。然而,有些node模块直接或间接使用原生C/C++代码,这些东西要跨平台,就需要使用源码根据实际的操作平台环境进行原生模块编译。通常我们开发环境为macOS或Windows,而生产环境为Linux的各种发行版,这将导致我们的开发工作变得沉重不堪。那我们是否可以跳过node-gyp的编译过程?
node-gyp的编译是让人难受的过程,所以社区出现了node-pre-gyp
和prebuild-install
,它们都会优先下载插件作者预编译的二进制文件,当二进制文件下载出现问题时,再使用node-gyp进行编译兜底。但因为我们网络环境的特殊性,这些二进制文件我们大概率是不会下载成功的,接下来一起来看看在canvas的安装过程中node-pre-gyp
干了什么事。
关于prebuild-install
参考姊妹文【Nodejs】关于原生模块编译node-gyp + prebuild-install (以安装better-sqlite3为例)
canvas就使用了node-pre-gyp来优化构建过程
关于install我们需要了解一点东西, 通常基于表象我们都会认为npm下载解压并释放到node_modules后就完成了安装过程,但实际上npm还会检查package中是否配置了install命令,存在就会立即执行,这也是原生模块在下载完成后会自动构建的基础。
npm install canvas
可以看到canvas配置了install命令,所以npm下载canvas后立即执行了node-pre-gyp install --fallback-to-build --update-binary
{
...,
"scripts": {
"prebenchmark": "node-gyp build",
"benchmark": "node benchmarks/run.js",
"lint": "standard examples/*.js test/server.js test/public/*.js benchmarks/run.js lib/context2d.js util/has_lib.js browser.js index.js",
"test": "mocha test/*.test.js",
"pretest-server": "node-gyp build",
"test-server": "node test/server.js",
"generate-wpt": "node ./test/wpt/generate.js",
"test-wpt": "mocha test/wpt/generated/*.js",
"install": "node-pre-gyp install --fallback-to-build --update-binary",
"dtslint": "dtslint types"
}
}
node-pre-gyp命令最终链接到了@mapbox/node-pre-gyp/lib/main.js
,根据参数install
最终进入@mapbox/node-pre-gyp/lib/install.js
执行
可以看到node-pre-gyp
先检查项目本地是否已经存在二进制构建文件,当不存在时进入用户本地查找,当用户本地也不存在时会执行http下载任何,接下来我们在看看http链接如何生成
existsAsync(binary_module, (found) => {
if (!update_binary) {
if (found) {
console.log('[' + package_json.name + '] Success: "' + binary_module + '" already installed');
console.log('Pass --update-binary to reinstall or --build-from-source to recompile');
return callback();
}
log.info('check', 'checked for "' + binary_module + '" (not found)');
}
makeDir(to).then(() => {
const fileName = from.startsWith('file://') && from.slice('file://'.length);
if (fileName) {
extract_from_local(fileName, to, after_place);
} else {
place_binary(from, to, opts, after_place);
}
}).catch((err) => {
after_place(err);
});
function after_place(err) {
if (err && should_do_fallback_build) {
print_fallback_error(err, opts, package_json);
return do_build(gyp, argv, callback);
} else if (err) {
return callback(err);
} else {
console.log('[' + package_json.name + '] Success: "' + binary_module + '" is installed via remote');
return callback();
}
}
});
其中from变量即预构件二进制文件地址,指向opts.hosted_tarball,源码只保留了核心部分,完整源码请查阅@mapbox/node-pre-gyp/lib/util/versioning.js
const host = process.env['npm_config_' + validModuleName + '_binary_host_mirror'] || package_json.binary.host;
opts.host = fix_slashes(eval_template(host, opts));
...
opts.remote_path = package_json.binary.remote_path ? drop_double_slashes(fix_slashes(eval_template(package_json.binary.remote_path, opts))) : default_remote_path;
...
opts.hosted_path = url.resolve(opts.host, opts.remote_path);
opts.hosted_tarball = url.resolve(opts.hosted_path, opts.package_name);
可以看到node-pre-gyp
会读取配置文件中是否配置了{包名}_binary_host_mirror
,否则读取待构建的插件package.json中的binary.host
配置项,所以在prebuild-install
中可以生效的{p包名}_binary_host
在node-pre-gyp
中是无效的,所以针对原生插件我们需要查看插件使用的node-pre-gyp
还是prebuild-install
来灵活调整.npmrc
中的预构件二进制文件下载镜像源
npm提供了.npmrc
配置文件并注入到进程的env环境变量中,从上面的源码可知,node-pre-gyp
会优先读取npm_config_{包名}_binary_host_mirror
(.npmrc中的变量均会被npm添加npm_config_前缀, 所以我们配置时无需添加npm_config_前缀),另外需要值得注意的是npm会将.npmrc
中的键以下划线的方式组织且任何非数字和字母的字符将会被替换为_
。所以以canvas举例来说,配置如下
canvas_binary_host_mirror=https://registry.npmmirror.com/-/binary/canvas
鉴于prebuild-install
兼容性更好,针对原生模块我们在.npmrc
中以{包名}_binary_host_mirror={mirror}
的格式配置预构建文件下载镜像