将预编译的二进制文件捆绑到电子应用程序中

bundling precompiled binary into electron app

关于如何将第三方预编译二进制文件(如 imagemagick)包含到电子应用程序中,是否有好的解决方案?有 node.js 个模块,但它们都是包装器或本机绑定到系统范围内安装的库。我想知道是否可以在发行版中捆绑预编译的二进制文件。

tl;博士:

是的,你可以!但它需要您编写自己的 self-contained 插件,它不会对系统库做出任何假设。此外,在某些情况下,您必须确保您的插件是为所需的 OS.

编译的

让我们把这个问题分成几个部分:

- Addons(本机模块):

Addons are dynamically linked shared objects.

换句话说,您可以编写自己的插件,而不依赖于系统范围的库(例如,通过静态链接所需的模块),其中包含您需要的所有代码。

你必须考虑这种方法是 OS-specific,这意味着你需要为每个你想要支持的 OS 编译你的插件! (取决于您可能使用的其他库)

- Native modules for electron:

The native Node modules are supported by Electron, but since Electron is using a different V8 version from official Node, you have to manually specify the location of Electron's headers when building native modules

这意味着必须重建针对节点 headers 构建的本机模块才能在电子内部使用。您可以在 electron 文档中找到操作方法。

- 将模块与电子应用捆绑在一起:

我想你想让你的应用程序成为一个 stand-alone 可执行文件,而不需要用户在他们的机器上安装 electron。如果是这样,我可以建议使用 electron-packager.

参见下面的UPDATE(这种方法现在并不理想)。

我确实找到了解决这个问题的方法,但我不知道这是否被认为是最佳实践。我找不到包含第 3 方预编译二进制文件的任何好的文档,所以我只是摆弄它,直到它最终与我的 ffmpeg 二进制文件一起使用。这是我所做的(从电子快速入门开始,node.js v6):

Mac OS X法

我从应用程序目录 运行 在终端中使用以下命令将 ffmpeg 二进制文件作为模块包含在内:

mkdir node_modules/ffmpeg
cp /usr/local/bin/ffmpeg node_modules/ffmpeg/
cd node_modules/.bin
ln -s ../ffmpeg/ffmpeg ffmpeg

(将 /usr/local/bin/ffmpeg 替换为您当前的二进制路径,从此处下载)放置 link 允许 electron-packager 包含我保存到 node_modules/ffmpeg/.[=28 的二进制文件=]

然后获取捆绑的应用程序路径(这样我就可以为我的二进制文件使用绝对路径......无论我做什么,相对路径似乎都不起作用)我安装了 npm 包 app-root-通过 运行 执行以下命令创建目录:

npm i -S app-root-dir

现在我有了根应用程序目录,我只需附加我的二进制文件的子文件夹并从那里生成。这是我放在 renderer.js:.

中的代码
var appRootDir = require('app-root-dir').get();
var ffmpegpath=appRootDir+'/node_modules/ffmpeg/ffmpeg';
console.log(ffmpegpath);

const
    spawn = require( 'child_process' ).spawn,
    ffmpeg = spawn( ffmpegpath, ['-i',clips_input[0]]);  //add whatever switches you need here

ffmpeg.stdout.on( 'data', data => {
     console.log( `stdout: ${data}` );
    });
   ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
    });

Windows方法

  1. 打开你的 electron 基础文件夹(electron-quick-start 是默认名称),然后进入 node_modules 文件夹。在那里创建一个名为 ffmpeg 的文件夹,并将静态二进制文件复制到该目录中。注意:它必须是二进制文件的静态版本,对于 ffmpeg,我抓取了最新的 Windows build here.

  2. 为了获取捆绑的应用程序路径(这样我就可以为我的二进制文件使用绝对路径......无论我做什么,相对路径似乎都不起作用)我安装了 npm 包app-root-dir 通过 运行 在我的应用程序目录中的命令提示符下执行以下命令:

     npm i -S app-root-dir
    
  3. 在您的 node_modules 文件夹中,导航到 .bin 子文件夹。你需要在这里创建几个文本文件来告诉节点包含你刚刚复制的二进制 exe 文件。使用您最喜欢的文本编辑器创建两个文件,一个名为 ffmpeg,内容如下:

    #!/bin/sh
    basedir=$(dirname "$(echo "[=14=]" | sed -e 's,\,/,g')")
    
    case `uname` in
        *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
    esac
    
    if [ -x "$basedir/node" ]; then
      "$basedir/node"  "$basedir/../ffmpeg/ffmpeg" "$@"
      ret=$?
    else
      node  "$basedir/../ffmpeg/ffmpeg" "$@"
      ret=$?
    fi
    exit $ret
    

第二个文本文件,名为 ffmpeg.cmd:

    @IF EXIST "%~dp0\node.exe" (
     "%~dp0\node.exe"  "%~dp0\..\ffmpeg\ffmpeg" %*
    ) ELSE (
       @SETLOCAL
     @SET PATHEXT=%PATHEXT:;.JS;=;%
     node  "%~dp0\..\ffmpeg\ffmpeg" %*
    )

接下来,您可以 运行 ffmpeg 在您的 Windows 电子分布(在 renderer.js 中)如下(我也在使用 app-root-dir 节点模块)。请注意添加到二进制路径的引号,如果您的应用程序安装到带有空格的目录(例如 C:\Program Files\YourApp),则没有这些将无法工作。

var appRootDir = require('app-root-dir').get();
var ffmpegpath = appRootDir + '\node_modules\ffmpeg\ffmpeg';

const
    spawn = require( 'child_process' ).spawn;
    var ffmpeg = spawn( 'cmd.exe', ['/c',  '"'+ffmpegpath+ '"', '-i', clips_input[0]]);  //add whatever switches you need here, test on command line first
ffmpeg.stdout.on( 'data', data => {
     console.log( `stdout: ${data}` );
 });
ffmpeg.stderr.on( 'data', data => {
     console.log( `stderr: ${data}` );
 });

更新:统一简单方法

好吧,随着时间的推移和 Node 的更新,这种方法不再是包含预编译二进制文件的最简单方法。它仍然有效,但是当 npm install 为 运行 时, node_modules 下的二进制文件夹将被删除并且必须再次替换。以下方法适用于 Node v12。

这种新方法避免了对 symlink 的需要,并且对 Mac 和 Windows 的工作方式类似。相对路径现在似乎可以工作了。

  1. 您仍然需要 appRootDir:npm i -S app-root-dir

  2. 在您的应用程序根目录下创建一个名为 bin 的文件夹,并将预编译的静态二进制文件放在这里,我以 ffmpeg 为例。

  3. 在渲染器脚本中使用以下代码:

const appRootDir = require('app-root-dir').get();
const ffmpegpath = appRootDir + '/bin/ffmpeg';
const spawn = require( 'child_process' ).spawn;
const child = spawn( ffmpegpath, ['-i', inputfile, 'out.mp4']);  //add whatever switches you need here, test on command line first
child.stdout.on( 'data', data => {
    console.log( `stdout: ${data}` );
});
child.stderr.on( 'data', data => {
    console.log( `stderr: ${data}` );
});

这是另一种方法,目前已使用 Mac 和 Windows 进行了测试。需要 'app-root-dir' 包,不需要手动添加任何东西到 node_modules 目录。

  1. 将您的文件放在 resources/$os/ 下,其中 $os"mac""linux""win".构建过程将根据构建目标 OS.

  2. 从 those 目录复制文件
  3. extraFiles 选项放入您的构建配置中,如下所示:

package.json

  "build": {
    "extraFiles": [
      {
        "from": "resources/${os}",
        "to": "Resources/bin",
        "filter": ["**/*"]
      }
    ],
  1. 使用类似这样的东西来确定当前平台。

get-platform.js

import { platform } from 'os';

export default () => {
  switch (platform()) {
    case 'aix':
    case 'freebsd':
    case 'linux':
    case 'openbsd':
    case 'android':
      return 'linux';
    case 'darwin':
    case 'sunos':
      return 'mac';
    case 'win32':
      return 'win';
  }
};
  1. 根据 env 和 OS 从您的应用程序调用可执行文件。这里我假设构建版本处于生产模式,源版本处于其他模式,但您可以创建自己的调用逻辑。
import { join as joinPath, dirname } from 'path';
import { exec } from 'child_process';

import appRootDir from 'app-root-dir';

import env from './env';
import getPlatform from './get-platform';

const execPath = (env.name === 'production') ?
  joinPath(dirname(appRootDir.get()), 'bin'):
  joinPath(appRootDir.get(), 'resources', getPlatform());

const cmd = `${joinPath(execPath, 'my-executable')}`;

exec(cmd, (err, stdout, stderr) => {
  // do things
});

我想我是在使用 electron-builder 作为基础,env 文件生成随它而来。基本上它只是一个 JSON 配置文件。

以上答案帮助我弄清楚了它是如何完成的,但是有一种更有效的方法来分发二进制文件。

根据 的提示,这是我的代码:

注意:根据需要替换或添加OS路径。

更新 - 2020 年 12 月 4 日

此答案已更新。找到此答案底部的上一个代码。

  • 下载需要的包
    yarn add electron-root-path electron-is-packaged
    
    # or 
    
    npm i electron-root-path electron-is-packaged
  • 创建目录./resources/mac/bin
  • 将二进制文件放入此文件夹
  • 创建文件./app/binaries.js并粘贴以下代码:
    import path from 'path';
    import { rootPath as root } from 'electron-root-path';
    import { isPackaged } from 'electron-is-packaged';
    import { getPlatform } from './getPlatform';
    
    const IS_PROD = process.env.NODE_ENV === 'production';
    
    const binariesPath =
      IS_PROD && isPackaged // the path to a bundled electron app.
        ? path.join(root, './Contents', './Resources', './bin')
        : path.join(root, './build', getPlatform(), './bin');
    
    export const execPath = path.resolve(
      path.join(binariesPath, './exec-file-name')
    );
  • 创建文件./app/get-platform.js并粘贴以下代码:

    'use strict';
    
    import { platform } from 'os';
    
    export default () => {
      switch (platform()) {
        case 'aix':
        case 'freebsd':
        case 'linux':
        case 'openbsd':
        case 'android':
          return 'linux';
        case 'darwin':
        case 'sunos':
          return 'mac';
        case 'win32':
          return 'win';
      }
    };

  • ./package.json 文件中添加这些行:

    "build": {
    ....
    
     "extraFiles": [
          {
            "from": "resources/mac/bin",
            "to": "Resources/bin",
            "filter": [
              "**/*"
            ]
          }
        ],
    
    ....
    },

  • 将二进制文件导入为:

    import { execPath } from './binaries';
        
    #your program code:
    var command = spawn(execPath, arg, {});

为什么这样更好?

  • 以上答案需要一个名为 app-root-dir

    的附加包
  • tsuriga 的回答没有正确处理 (env=production) buildpre-packed 版本. He/she 只负责开发和 post 打包版本。

上一个回答

  • 避免使用 electron.remote,因为它正在贬值
  • app.getAppPath 可能会在主进程中抛出错误。

./app/binaries.js

    'use strict';
    
    import path from 'path';
    import { remote } from 'electron';
    import getPlatform from './get-platform';
    
    const IS_PROD = process.env.NODE_ENV === 'production';
    const root = process.cwd();
    const { isPackaged, getAppPath } = remote.app;
    
    const binariesPath =
      IS_PROD && isPackaged
        ? path.join(path.dirname(getAppPath()), '..', './Resources', './bin')
        : path.join(root, './resources', getPlatform(), './bin');
    
    export const execPath = path.resolve(path.join(binariesPath, './exec-file-name'));

遵循 Ganesh 的回答,这真的是一个很大的帮助,在我的例子中,binaries.js 中的工作(对于 mac 构建 - 没有测试 windows 或 linux) 是:

"use strict";
import path from "path";
import { app } from "electron";

const IS_PROD = process.env.NODE_ENV === "production";
const root = process.cwd();
const { isPackaged } = app;

const binariesPath =
  IS_PROD && isPackaged
    ? path.join(process.resourcesPath, "./bin")
    : path.join(root, "./external");

export const execPath = path.join(binariesPath, "./my_exec_name");

考虑到 my_exec_name 在文件夹 ./external/bin 中并复制到 ./Resources/bin 中的应用程序包中。我没有使用 get_platforms.js 脚本(在我的情况下不需要)。 app.getAppPath() 在打包应用程序时发生崩溃。 希望对你有帮助。

主要基于 Ganesh 的回答,但有所简化。此外,我正在使用 Vue CLI Electron Builder Plugin,因此配置必须位于略有不同的位置。

  1. 创建一个 resources 目录。将所有文件放在那里。
  2. 将此添加到 vue.config.js:
module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        ...
        "extraResources": [
          {
            "from": "resources",
            "to": ".",
            "filter": "**/*"
          }
        ],
        ...
      }
    }
  }
}
  1. 在您的 src 文件夹中创建一个名为 resources.ts 的文件,内容如下:
import path from 'path';
import { remote } from 'electron';

// Get the path that `extraResources` are sent to. This is `<app>/Resources`
// on macOS. remote.app.getAppPath() returns `<app>/Resources/app.asar` so
// we just get the parent directory. If the app is not packaged we just use
// `<current working directory>/resources`.
export const resourcesPath = remote.app.isPackaged ?
                             path.dirname(remote.app.getAppPath()) :
                             path.resolve('resources');

请注意,我尚未在 Windows/Linux 上对此进行测试,但假设 app.asar 在这些平台上的资源目录中(我假设如此),它应该可以工作。

  1. 这样使用:
import { resourcesPath } from '../resources'; // Path to resources.ts

...
    loadFromFile(resourcesPath + '/your_file');