Electron项目中编译基于Sqlcipher的Sqlite3

2022-05-10 00:00:00 版本 指定 编译 提供 模块

为Electron项目中的原生模块进行重编译,已经做过几次了。这次升级Electron和Sqlcipher版本,自然需要重编译下Sqlite3。结果依然花了不少时间,决定再做一次整理。

编译环境

node - 10.15.x

electron - 5.0.8

sqlite3 - 5.0.0

sqlcipher - 4.3

要解决的问题

  • 环境

    全局安装需要的electron版本和node-gyp

    一般原生模块的文档中都会提供所需环境配置手册。如编译sqlite3需要的sqlcipher

  • 分发

    官方提供的配置文件,一般不会考虑分发的问题,因此依赖库都是通过动态链接。这样做的好处很显然:配置简单、生成的模块文件小。但并不适用需要分发的Electron项目,手动修改配置文件,指定依赖的静态文件是主要难点。

下载 node-sqlite3

npm i sqlite3 --ignore-scripts

之所以加上--ignore-scripts,是因为大多原生模块的脚本中都写了下载时自动执行编译的脚本,例如node-sqlite3:

"scripts": {
    "install": "node-pre-gyp install --fallback-to-build",
  },
复制代码

往往这时候编译环境还没准备好,而且我们需要的是基于Electron和sqlcipher的版本,自动编译的是基于当前node环境的版本。如果编译失败了,那会导致整个包都无法下载下来。

Node原生模块通过binding.gyp文件指定在不同平台下所需要的依赖:

{
  "includes": [ "deps/common-sqlite.gypi" ],
  "variables": {
      "sqlite%":"internal",
      "sqlite_libname%":"sqlite3"
  },
  "targets": [
    {
      "target_name": "<(module_name)",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "xcode_settings": { "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
        "CLANG_CXX_LIBRARY": "libc++",
        "MACOSX_DEPLOYMENT_TARGET": "10.7",
      },
      "msvs_settings": {
        "VCCLCompilerTool": { "ExceptionHandling": 1 },
      },
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"],
      "conditions": [
        ["sqlite != 'internal'", {
            "include_dirs": [
              "<!@(node -p \"require('node-addon-api').include\")", "<(sqlite)/include" ],
            "libraries": [
               "-l<(sqlite_libname)"
            ],
            "conditions": [ [ "OS=='linux'", {"libraries+":["-Wl,-rpath=<@(sqlite)/lib"]} ] ],
            "conditions": [ [ "OS!='win'", {"libraries+":["-L<@(sqlite)/lib"]} ] ],
            'msvs_settings': {
              'VCLinkerTool': {
                'AdditionalLibraryDirectories': [
                  '<(sqlite)/lib'
                ],
              },
            }
        },
        {
            "dependencies": [
              "<!(node -p \"require('node-addon-api').gyp\")",
              "deps/sqlite3.gyp:sqlite3"
            ]
        }
        ]
      ],
      "sources": [
        "src/backup.cc",
        "src/database.cc",
        "src/node_sqlite3.cc",
        "src/statement.cc"
      ],
      "defines": [ "NAPI_VERSION=<(napi_build_version)", "NAPI_DISABLE_CPP_EXCEPTIONS=1" ]
    },
    {
      "target_name": "action_after_build",
      "type": "none",
      "dependencies": [ "<(module_name)" ],
      "copies": [
          {
            "files": [ "<(PRODUCT_DIR)/<(module_name).node" ],
            "destination": "<(module_path)"
          }
      ]
    }
  ]
}
复制代码

从bingding.gyp中可以看出我们所需要的依赖,这点也可以通过官方ReadMe文档获知。

在node-sqlite3的binding.gyp中,有一些执行编译时需要指定的参数:

sqlite_libname : 依赖库,如基于sqlcipher编译时,sqlite_libname=sqlcipher

napi_build_version: 指定napi的版本,当前指定的3

module_name: 生成原生模块的名称,默认node_sqlite3

module_path: 指定原生模块生成位置,这里路径必须写对,后续node引入时,sqlite3的入口文件会根据调用环境解析原生模块的路径。如在mac上编译基于Electron 5.0.x的模块时,路径应为:../lib/binding/electron-v8.3-darwin-x64

于是,手动重编译的命令:

node-gyp rebuild --runtime=electron --target=5.0.8 --sqlite_libname=sqlcipher --dist-url=https://electronjs.org/headers --module_path=../lib/binding/electron-v5.0-darwin-x64 --module_name=node_sqlit3 --napi_build_version=3

MacOS

  1. HomeBrew 下载 Sqlcipher

    brew install sqlcipher

    笔者今年更换了MacBook Pro,homebrew等环境也都下载安装的新。发现HomeBrew已经不支持大多数option,也无法通过brew install sqlcipher@4.x 这样的命令来安装指定版本。好在,我们这次也决定升级sqlcipher。

  2. 指定静态文件路径

    在 binding.gyp 中,include_dirslibraries用于指定依赖库的路径。

    已知sqlcipher的存放路径:/usr/local/Cella/sqlcipher ,然后将对应mac平台配置中的值进行修改:

    "include_dirs": [
            "<!@(node -p \"require('node-addon-api').include\")", "/usr/local/Cellar/sqlcipher/4.3.0_1/include"],
          "conditions": [
            ["sqlite != 'internal'", {
                "include_dirs": [
                  "<!@(node -p \"require('node-addon-api').include\")", "<(sqlite)/include" ],
                "libraries": [
                   "/usr/local/Cellar/sqlcipher/4.3.0_1/lib/libsqlcipher.a"
                ],
                "conditions": [ [ "OS=='linux'", {"libraries+":["-Wl,-rpath=<@(sqlite)/lib"]} ] ],
                "conditions": [ [ "OS!='win'", {"libraries+":["/usr/local/Cellar/sqlcipher/4.3.0_1/lib/libsqlcipher.a"]} ] ],
                'msvs_settings': {
                  'VCLinkerTool': {
                    'AdditionalLibraryDirectories': [
                      '<(sqlite)/lib'
                    ],
                  },
                }
            },
    复制代码
  3. 重编译

    因已经将 binding.gyp 中变量 sqlite_libname的值写死,可以去掉上文编译命令中的这个参数:

    node-gyp rebuild --runtime=electron --target=5.0.8 --dist-url=https://electronjs.org/headers --module_path=../lib/binding/electron-v5.0-darwin-x64 --module_name=node_sqlit3 --napi_build_version=3

  4. 本地测试

    在Electron项目中引入编译得到的 node_sqlite3.node文件,对旧版的数据库文件进行连接测试。这里需要注意Sqlicpher不同版本带来的变化:

    旧版本下的数据库文件基于Sqlcipher 3.x,在使用新编译的模块去连接操作时,若按照原语法操作,会报错。

    这里有两种解决方案,具体见 Upgrading to SQLCipher 4

    1)数据库文件升级

    PRAGMA key = '<key material>';
    PRAGMA cipher_migrate;
    复制代码

    成功连上旧数据库文件后,通过 sqlcipher 4.x 提供的新PRAGMA cipher_migrate,可以将旧数据库文件的格式和设置升级,从而获得更好的性能。

    虽然数据库迁移语句只需要执行一次,但迁移过程的耗时不可控,建议增加提示的交互,迁移完成后再打开Electron窗口。(在windows上测试时,主窗口直接无响应了很长时间,mac倒没有..)

    p.s 官方虽然提供了迁移语句,但迁移过程也有可能失败,所以也提供了备选方案:解开原数据库并导出非加密版数据库文件 plaintext.db,使用新sqlcipher 对 plaintext.db加密。该方案较为繁琐。

    2)使用降级语句

    PRAGMA key = '<key material>';
    PRAGMA cipher_compatibility = 3;
    复制代码

    好处是直接使用,没有耗时的迁移过程,但也无法得到新版本的性能提升。

    所以,我们选择方案1

  5. 分发测试

    Electron打包分发,自信满满的在其他mac设备上进行测试,结果被报错打脸。

    原因仍然出在动态链接上,但我们明明已经将依赖库sqlcipher指定成静态文件 libsqlcipher.a了...一番查issue和分析后,发现问题出在brew下载的 sqlcipher上。

    sqlcipher 的加解密依赖于OpenSSL库,而通过brew 安装的sqlcipher是基于openssl动态链接编译的,本地测试能通过的原因是我已经 brew install openssl过。

    解决方案 - 手动编译sqlcipher:

    参考:www.shuzhiduo.com/A/lk5aaDao5…

    discuss.zetetic.net/t/updating-…

    1. clone sqlicpher , github.com/sqlcipher/s…

    2. 编译源码

    make clean
    ./configure --enable-tempstore=yes --enable-load-extension --disable-tcl --with-crypto-lib=commoncrypto CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS5" LDFLAGS="-framework Security -framework Foundation"
    make
    复制代码
    1. 替换brew下载的sqlcipher

    成功后出现 .libs 隐藏文件夹,将生成的libsqlcipher.alibsqlcipher.0.dylib复制到/usr/local/Cella/sqlciher/xxx/lib下去替换。

    然后,按上述binding.gyp修改方式,引入静态文件,进行重编译。

Windows

在windows上,除了基本的electron和node-gyp,还需要全局安装windows-build-tools和 python 2.x,以及 visual studio相关工具集。

旧方案

在node-sqlite3 和 sqlcipher 的相关文档中,并没有详细的关于windows平台编译方案。之前项目中使用的是win-sqlcipher,该项目作者fork了一份带有OpenSSL源码的node-sqlite3版本,并通过node-pre-gyp实现了下载后自动编译。

然后,在win-sqlcipher下再通过node-gyp指定electron版本和架构(ia32或x64),重编译得到需要的原生模块文件。

win-sqlcipher和相关repository已经很久没有更新,不能满足使用Sqlcipher 4.x的需求。

新方案

1)手动编译 sqlcipher

windows平台基于sqlcipher源码的编译方案,翻了很久,没有找到完整有效的流程,暂时放弃。

2)基于 node-sqlcipher

node-sqlcipher与 win-sqlcipher类似,也是fork了node-sqlite3,然后在package.json中,将node-pre-gyp 指向自己的二进制文件目录。

作者当前提供的正好是基于sqlcipher 4.3版本,符号我们的需求。同时也提供了自定义sqlcipher版本的操作方法。SQLCipher.md

使用node-sqlcipher的方法很简单,作者已经提供了基于静态OpenSSL编译的Sqlcipher。笔者遇到的坑是缺少 Visual Studio 2015 环境。好在预装了 Visual Studio 2019,可以在IDE中选择 2015 工具集进行安装。

node-sqlcipher也提供了mac环境基于静态编译的sqlcipher,这里没有继续尝试,后续升级可能会再试下。

总结

Electron项目中,原生模块的编译虽然麻烦,但掌握其流程和原理很有必要。无论是引入开源的 Nodejs原生模块,还是集成其他厂商提供的原生SDK。

原生模块基于C/C++,体积和性能上优于 nodejs 模块,而且二进制的文件格式,代码安全性更高。飞书的桌面端也是基于Electron,其应用中就大量使用了原生模块,而且基本都是自用的模块。

相关文章