使用 nexe 进行跨平台编译
最近遇到一个小需求,希望写一个 Windows 可执行文件,做一些监听 U 盘插入事件的事情。由于不需要 GUI,electron 就用不到了。不过仍然需要打包 Node.js 执行环境进来。
一番搜索之后找到了 nexe 这个库。简单介绍一下它的几个关键特性:
- 支持命令行和编程式调用两种使用方式
- 支持选择构建目标为不同操作系统
- 支持打包不同 Node.js 版本
下面我们看一下它的使用方法。
内置打包工具
首先默认使用的打包工具是 Fusebox,但是也支持通过 bundle
选项切换成其他构建工具。例如我们使用自定义的构建方法:
// build.js
const nexe = require('nexe');
nexe.compile({
output: 'native-build',
target: 'win32-x64-8.9.4',
bundle: './nexe-bundle.js',
silent: false
});
在构建方法中,我们就可以使用任意构建工具编译 JS 文件了,只要最后返回最终结果。 这里我们使用 Webpack 4:
// nexe-bundle.js
const webpack = require('pify')(require("webpack"))
const fs = require('fs')
module.exports.createBundle = function (options) {
return webpack({
entry: options.input,
target: 'node',
output: { filename: 'dist/tmp.js' }
}).then(() => {
const result = fs.readFileSync('./dist/tmp.js').toString()
fs.unlinkSync('./dist/tmp.js')
return result
})
}
选择目标平台
nexe 支持 Mac Windows Linux 平台下的构建。通过 target: 'win32-x64-8.9.4'
我们可以选择操作系统以及 Node.js 版本。
全部可用的列表在这里。
首次构建时会下载目标平台下的运行环境,并进行缓存。
需要注意的是,由于某些第三方库依赖当前运行环境,所以要想编译跨平台的程序最好在目标平台上进行构建。 例如在 Mac 上想要构建 Windows 下的 exe,最好安装虚拟机,比如 Parallel Desktop。
node-gyp
Node.js 虽然强大,但是在使用某些平台底层的功能时,还是需要依赖 C++ 编写的组件。在 Node.js 中,可以通过 node-gyp 将这些原生代码编译成 node 模块,在运行时很方便地进行调用。
以 node-usb-detection 为例,在 binding.gyp
文件中:
{
"targets": [
{
"target_name": "detection",
"sources": [
"src/detection.cpp",
"src/detection.h",
"src/deviceList.cpp"
],
"include_dirs" : [
"<!(node -e \"require('nan')\")"
],
'conditions': [
['OS=="win"',
{
'sources': [
"src/detection_win.cpp"
],
'include_dirs+':
[
# Not needed now
]
}
],
['OS=="mac"',
{
'sources': [
"src/detection_mac.cpp"
],
"libraries": [
"-framework",
"IOKit"
]
}
]
]
}
]
}
更详细的例子可以参考 Node.js addons 文档。
Windows 上的可怕经历
在 Windows 虚拟机上的编译经历可谓困难重重。
首先按照 node-gyp
的安装说明,执行:
npm install --global --production windows-build-tools
这一步会安装 Python 和 VS 构建工具。这时候可以检查下 C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0
下是否有 v140
也就是 VS2015 的构建工具。
一切顺利的话就可以开始安装 npm 依赖了,很多使用了 node-gyp
的第三方依赖此时会进行 prebuilt-install
,开始构建 node addon。
运行时如果出现如下错误 MSB4019
:
error MSB4019: The imported project “C:\Microsoft.Cpp.Default.props” was not found. Confirm that the path in the
declaration is correct, a nd that the file exists on disk.
需要在 CMD 中设置环境变量,这里我们设置成之前安装好的 VS2015 的路径。相关 ISSUE:
SET VCTargetsPath=C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\v140
如果出现如下错误 MSB8036
:
MSB8036: The Windows SDK version 8.1 was not found. Install the required version of Windows SDK
则需要配置 npm 环境变量,相关 ISSUE
npm config set msvs_version 2015
总之,遇到问题可以先在 MS 官方的指导意见 中查找,能少走一些弯路。
示例:监听 U 盘插拔
回到我们最初的需求,希望监听 U 盘的插拔。
启动监听后,进程不会退出,类似 DOS 中的 pause
语句。
在监听到 add
事件时,回调函数会传入插入 USB 设备的对象。
const usbDetect = require('usb-detection');
usbDetect.startMonitoring();
usbDetect.on('add', device => {});
值得注意的是在这个设备对象中,是不包含挂载点的,更多的是一些底层设备信息。 一些更高层次得操作,例如试图获取挂载点,并没有提供,相关ISSUE。
{
locationId: 0,
vendorId: 5824,
productId: 1155,
deviceName: 'Teensy USB Serial (COM3)',
manufacturer: 'PJRC.COM, LLC.',
serialNumber: '',
deviceAddress: 11
}
为了获取当前得挂载路径,不得不借助其他库,例如 drivelist
。
通过定时遍历当前所有驱动设备,我们能找出其中的 USB 存储设备,得到其挂载点。
之所以使用定时器是因为挂载需要时间,触发 USB 设备的 add
事件时还没有挂载好。
const drivelist = require('drivelist');
let checkUSBIntervalID;
checkUSBIntervalID = setInterval(() => {
drivelist.list((error, drives) => {
if (error) {
throw error;
}
drives.forEach(drive => {
if (!drive.isSystem && drive.isRemovable
&& drive.mountpoints.length) {
clearInterval(checkUSBIntervalID);
scanDrive(drive);
}
});
});
}, 1000);
得到了挂载点信息,就可以使用 fs
模块进行文件操作了:
mountpoints: [ { path: '/Volumes/KINGSTON', label: 'KINGSTON' } ],
查找 node addon
最后我们关注一下 nexe 中的一个技术细节。
使用 node-gyp
编译之后得到 node addon,可以通过 bindings
模块使用。
例如之前介绍过的 usb-detection
是这么使用的:
var detection = require('bindings')('detection.node');
那么 nexe
是如何解决运行时 node addon 路径查找的呢?
按照官方文档的说法,编译之后的 .node
文件会生成在 build/Release/
目录下,解析路径时也会默认从这里开始查找。
Next, invoke the node-gyp build command to generate the compiled addon.node file. This will be put into the build/Release/ directory.
nexe 利用 Fusebox 的转译插件(类似 Babel),对代码中的 bindings
路径进行了重写。