node-pre-gyp以及node-gyp的源碼簡單解析(以安裝sqlite3為例)



title: node-pre-gyp以及node-gyp的源碼簡單解析(以安裝sqlite3為例)
date: 2020-11-27
tags:

  • node
  • native
  • sqlite3

前言

簡單來說,node是跨平台的,那么對於任何的node模塊理論也是應該是跨平台的。然而,有些node模塊直接或間接使用原生C/C++代碼,這些東西要跨平台,就需要使用源碼根據實際的操作平台環境進行原生模塊編譯。SQLite3就是一個經典的原生模塊,讓我們以安裝該模塊為例,探索一下安裝原生模塊的流程。

項目建立

建立一個簡單的node項目,我們開始安裝SQLite3

$ mkdir sqlite3-install-demo 
$ cd sqlite3-install-demo
$ npm init
# 初始化項目
Press ^C at any time to quit.
package name: (projects) sqlite3-install-demo
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC) MIT
About to write to D:\Projects\package.json:

{
  "name": "sqlite3-install-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}

安裝SQLite3

$ npm install -S sqlite3

完成命令執行后,你會看到命令行界面出現了如下的幾行重要的輸出:

...
> sqlite3@5.0.0 install D:\Projects\sqlite3-install-demo\node_modules\sqlite3
> node-pre-gyp install --fallback-to-build

node-pre-gyp WARN Using request for node-pre-gyp https download
...

啪一下,很快啊!我們就迎來了第一個東西node-pre-gyp,但是提到了node-pre-gyp,我們不得不提及node-gyp,然后又不得不提及gyp

gyp與node-gyp與node-pre-gyp

什么是gyp?

gyp全稱Generate Your Projects(構建你的項目)。wiki的解釋如下,自行翻譯:

GYP (generate your projects) is a build automation tool. GYP was created by Google to generate native IDE project files (such as Visual Studio Code and Xcode) for building the Chromium web browser and is licensed as open source software using the BSD software license.

重點在於,它是一套用於生成原生IDE項目文件的自動化構建工具,處理C/C++項目,同類型的有CMake、ninja等自動構建工具。

什么是node-gyp?

直接給出stackoverflow高票回答:

node-gyp is a tool which compiles Node.js Addons. Node.js Addons are native Node.js Modules, written in C or C++, which therefore need to be compiled on your machine. After they are compiled with tools like node-gyp, their functionality can be accessed via require(), just as any other Node.js Module.

簡單來說,node是跨平台的,那么對於任何的node模塊理論也是應該是跨平台的。然而,有些node模塊直接或間接使用原生C/C++代碼,這些東西要跨平台,就需要使用源碼根據實際的操作平台環境進行原生模塊編譯。那么我們需要下載源碼文件,通過node-gyp生成一定結構的代碼項目讓我們能夠require引入(譬如,Windows下會生成vcxproj,再調用MSBuild進行編譯,以生成Windows下的動態鏈接庫,最后打包為一個原生node模塊)。這個知乎回答的每一條可以看看:傳送門

什么是node-pre-gyp?

上面node-gyp固然相當方便了,但是每一次安裝node原生模塊的時候,都需要根據平台(Windows、Linux、macOS以及對應的x86、x64、arm64等等)進行源碼編譯,這樣做費時費力。為什么不一開始就針對這些平台編譯好了做成二進制制品發布呢?反正一般來說主流的平台架構就那么一些(Windows、Linux、macOS)。所以node-pre--gyp就幫我們做了這件事。原生模塊開發者將代碼編譯生成各個平台架構的二進制包直接發布到node-pre-gyp上,當我們的node項目安裝原生模塊時候。處理流程就是首先去node-pre-gyp上找有沒有當前平台的組件包,有的話直接拉取使用,如果沒有則進行原生編譯。

node-pre-gyp一些重要參數(不全):

  • -C/--directory: run the command in this directory
  • --build-from-source: build from source instead of using pre-built binary
  • --fallback-to-build: fallback to building from source if pre-built binary is not available
  • --target=0.4.0: Pass the target node or node-webkit version to compile against
  • --target_arch=ia32: Pass the target arch and override the host arch. Valid values are 'ia32','x64', or arm.
  • --target_platform=win32: Pass the target platform and override the host platform. Valid values are linux, darwin, win32, sunos, freebsd, openbsd, and aix.

對於--fallback-to-build這個參數:如果二進制不可獲取則直接從源碼編譯,即從node-pre-gyp又回到node-gyp。所以你才會在上文看到安裝sqlite3的時候,會有--fallback-to-build

於是乎,當我們進行node原生模塊安裝的時候,一般會有如下的流程:

  1. 針對當前平台架構優先考慮node-pre-gyp方式進行安裝,但是為了防止無法獲取針對對應平台編譯好的二進制包(網絡原因、暫時沒有對應平台的二進制包),進入第2步;
  2. 下載原生模塊源碼,然后使用node-gyp進行項目構建,得到與平台相關的源碼項目文件(Windows則生成vcxproj項目,Linux下是Makefile);在這個過程,node-gyp會使用Python進行自動化構建操作,這也是為什么有些朋友安裝node原生模塊的時候,會報錯找不到Python
  3. 調用平台對應的編譯工具進行編譯。在Windows的環境下,node-gyp會查找本地的MSBuild/CL等編譯工具,而這些編譯工具又一般在Visual Studio安裝的時候,也一並安裝在了機器上。這就是為什么有些朋友沒有安裝Visual Studio的時候,會報錯。

探索SQLite3的安裝流程

npm install

為什么我們安裝sqlite3的時候,會調用node-pre-gyp命令呢?進入項目目錄/node_modules/sqlite3/文件夾,讓我們查看一下package.json中的scripts部分:

{
  ...
  "repository": {
    "type": "git",
    "url": "git://github.com/mapbox/node-sqlite3.git"
  },
  "scripts": {
    "install": "node-pre-gyp install --fallback-to-build", // install
    "pack": "node-pre-gyp package",
    "pretest": "node test/support/createdb.js",
    "test": "mocha -R spec --timeout 480000"
  },
  "version": "5.0.0"
}

答案顯而易見了,install腳本中執行了node-pre-gyp install --fallback-to-build命令。

這就不得不提到npm的安裝流程是。當我們進行npm install xxx的時候,npm首先下載xxx的包。下載完成后,若package.json中的scripts中存在install屬性,則會立刻調用。至於scripts中的其他固定腳本:testpreinstallpostinstall等等作用以及scripts的高級用法,請直接查閱scripts | npm Docs (npmjs.com)

所以本此sqlite3前期安裝的過程為:

  1. npm下載在倉庫中的sqlite3npm包;
  2. 執行${your_projects}/node_modules/sqlite3/package.json中的install腳本,即node-pre-gyp install --fallback-to-build

於是乎,安裝進入到了一個新的環節:node-pre-gyp install。當然,若你沒有全局安裝node-pre-gyp,它會由npm幫你安裝到${your_projects}/node_modules/中,並且通過node-pre-gyp/package.json中的bin元素,建立軟連接到${your_projects}/node_modules/.bin中。這樣,node\npm環境中就有了node-pre-gyp命令可以使用。至於package.json#bin的作用,詳細參考官方文檔package.json | npm Docs (npmjs.com)

node-pre-gyp install

node-pre-gyp在上述的安裝流程中,已經能夠被我們在CLI中所使用。查看node_modules/node-pre-gyp/bin/node-pre-gyp文件(下文都將省略${your_projects}/),用文本的形式打開。就是node-pre-gypCLI的執行過程,腳本中的主要內容為最后一行:

// start running the given commands!
run();

檢查該函數的定義:

function run () {
  var command = prog.todo.shift();
  if (!command) {
    // done!
    completed = true;
    log.info('ok');
    return;
  }

  prog.commands[command.name](command.args, function (err) {
    if (err) {
      log.error(command.name + ' error');
      log.error('stack', err.stack);
      errorMessage();
      log.error('not ok');
      console.log(err.message);
      return process.exit(1);
    }
    var args_array = [].slice.call(arguments, 1);
    if (args_array.length) {
      console.log.apply(console, args_array);
    }
    // now run the next command in the queue
    process.nextTick(run);
  });
}

prog是什么?該文件往上查看定義,原來是:

var node_pre_gyp = require('../'); // 上一個目錄作為模塊引入
var log = require('npmlog');

/**
 * Process and execute the selected commands.
 */

var prog = new node_pre_gyp.Run(); // 來自於node_pre_gyp中的Run,而node_pre_gyp在上方

繼續檢查上一個目錄,發現並沒又indes.js文件,熟悉npm的朋友應該知道要去看package.json中的main元素了:

...
	"license": "BSD-3-Clause",
	"main": "./lib/node-pre-gyp.js", // 模塊是這個文件
	"name": "node-pre-gyp",
...

查閱lib/node-pre-gyp.js代碼中的Run:

function Run() {
  var self = this;

  this.commands = {};

  commands.forEach(function (command) {
    self.commands[command] = function (argv, callback) {
      log.verbose('command', command, argv);
      return require('./' + command)(self, argv, callback); // 這里是核心
    };
  });
}

核心功能就是引入當前所在目錄下的模塊進行執行。例如,本次調用的是node-pre-gyp install,則會require(./install),檢查一下node-pre-gyp.js目錄下,果然存在該js文件。繼續閱讀install.js源碼。里面有幾個函數的定義。咱們先不看內容,把函數名列舉出來,猜測一下作用:

// 去下載平台編譯好的二進制?
function download(uri,opts,callback) {...}
// 把下載好的二進制放到對應目錄?
function place_binary(from,to,opts,callback) {...}
// 進行構建。難道是沒有下載,就調用node-gyp源碼編譯?
// 還有,node-pre-gyp又--fallback-to-build參數,也會調用這個?
function do_build(gyp,argv,callback) {...}
// 打印回退出現的異常
function print_fallback_error(err,opts,package_json) {...}
// 安裝,核心沒跑了
function install(gyp, argv, callback) {...}

首先看download的調用點是在place_binary中:

function place_binary(from,to,opts,callback) { // place_binary函數
    download(from,opts,function(err,req) { // 調用了download
        if (err) return callback(err);
        if (!req) return callback(new Error("empty req"));
		...
    }
        ...
}

再看place_binary調用點是在install中:

function install(gyp, argv, callback) {
	// 省略部分...
    var should_do_source_build = source_build === package_json.name || (source_build === true || source_build === 'true');
    if (should_do_source_build) { // 源碼編譯
        log.info('build','requesting source compile');
        return do_build(gyp,argv,callback);
    } else {
        // 省略部分...
        mkdirp(to,function(err) {
			if (err) {
				after_place(err);
			} else {
				place_binary(from,to,opts,after_place); // 調用點
			}
		});
        // 省略部分...
    }
    // 省略部分...
}

通過上述分析,整個大的處理流程如下:

  1. 進入install函數
  2. 檢查是否需要build-from-source。是則進,入do_build分支,進行源碼編譯;否則進入步驟3。
  3. 檢查是否啟用--fallback-to-build參數,設定是否啟用標志位。
  4. 解析編譯好的二進制文件的選項配置,譬如二進制文件存放地址,也就是通過請求下載對應二進制包的地址,以及各種各樣參數。所以說,為什么下載很慢,我們后文會重點關注下載地址。

下載二進制包

根據流程,接下來我們進一步檢查versioning.js文件,找到其中的evaluate函數,分析最后的hosted_tarball路徑:

hosted_tarball路徑主要分為兩個部分:1、hosted_path;2、package_name

hosted_path

經過源碼分析來源路徑為:

我們自底向上分析。

host變量取決於從環境變量中檢查名稱為'npm_config_' + opts.module_name + '_binary_host_mirror'的環境變量。如果不存在,則使用package_json.binary.host。正常使用的時候,我們並不會設定環境變量,所以這里就進入package_json.binary進行獲取。這個package_jsonevaluate函數被調用時候傳入的,在node-pre-gyp/install.js中能夠看到:

一開始分析的時候,看到這里,本人以為package_json就是node-pre-gyp/package.json,於是本人去檢查該json發現很奇怪,並沒有binary屬性,更別提host了。一番思考才明白,node-pre-gyp install的運行時調用者是誰呀?不是應該是sqlite3嗎?所以這個地方的require('./package.json')實際上是指代的是sqlite3/package.json。查看sqlite3/package.json,果然發現了對應的元素:

binary屬性中,我們還能看到remote_path也在其中。

至此,hosted_path我們完成了簡單的分析,我們可以得出一個結論:

node-pre-gyp下載二進制文件的路徑,優先來源於對應模塊的鏡像地址,該鏡像地址通過配置'npm_config_' + 模塊名 + '_binary_host_mirror'來實現自定義;在沒有定義鏡像地址的情況下,讀取模塊package.json中的binary屬性信息。

當然,讀者可以根據具體情況再進一步分析源碼。

package_name

其實,對於hosted_path的分析,我們也容易分析package_name了。

自底向上分析,來自於sqlite3/package.jsonbinary屬性中的package_name,內容見上圖分析host

失敗處理

--fallback-to-build參數表明了是否進行失敗后下載源碼進行編譯,源碼不再分析。

從源碼構建

build.js

當我們提供了參數--build-from-source或是在下載編譯好的二進制到本地出錯的時提供了參數--fallback-to-build。node-pre-gyp將進入do_build模塊,進行源碼編譯。

function do_build(gyp,argv,callback) {
  var args = ['rebuild'].concat(argv);
  gyp.todo.push( { name: 'build', args: args } );
  process.nextTick(callback);
}

代碼中,gyp由調用install的時候,傳入:

那么我們又將回到調用install的地方。實際上,gyp就是node-pre-gyp.js導出的模塊:

也就是說在do_build中進行操作就是,放置了一個build任務在隊列中。所以我們按照先前的分析,直接去看build.js

看源碼調用了當前模塊中的do_build,且其中最核心的就是compile模塊:

util/compile.js

進入compile模塊,直接找到對應的run_gyp函數,代碼很短,不難看出進行構建調用了node-gyp

上述代碼,會先考略node-webkit構建。但是我們核心的還是使用node-gyp,所以else中,會進行node-gyp的工具的檢查工作。最后調用命令行執行node-gyp。於是,node原生模塊的安裝工作,進入了新的階段:node-gyp

node-gyp build

上文提到我們已經進入了node-gyp的范疇,會調用node-gyp build操作。當然,這個命令同樣是在安裝node-gyp依賴的時候已經完成了安裝,並且進行node_modules/.bin/軟連接操作。

  ...
  "bin": {
    "node-gyp": "bin/node-gyp.js"
  },
  ...

我們進入該js進行分析

實際上,node-gyp這段的命令行代碼,和node-pre-gyp非常相似!所以我們也不去深入分析調用命令行了。直接在lib文件夾下面的build.js。在該js中,核心的方法為:

function build (gyp, argv, callback) {
	...
}

在該方法中,還編寫了幾個內部函數,作為了功能的划分:

// function build (gyp, argv, callback) 內部函數
	/**
   * Load the "config.gypi" file that was generated during "configure".
   */
  function loadConfigGypi () {...}
    /**
   * On Windows, find the first build/*.sln file.
   */
  function findSolutionFile () {...}
    /**
   * Uses node-which to locate the msbuild / make executable.
   */
  function doWhich () {...}
    /**
   * Search for the location of "msbuild.exe" file on Windows.
   */
  function findMsbuild () {...}
    /**
   * Actually spawn the process and compile the module.
   */
  function doBuild () {...}  
  /**
   * Invoked after the make/msbuild command exits.
   */
  function onExit (code, signal) {...}

不得不說,build寫的真心不錯,看起來很舒服。這里為了方便讀者快速閱讀,我整理這些函數的調用圖:

整個調用流程圖個人認為足夠進行安裝的時候的一場分析了。至於每個內部函數的功能,有空繼續更新本文吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM