從源碼分析node-gyp指定node庫文件下載地址


當我們安裝node的C/C++原生模塊時,涉及到使用node-gyp對C/C++原生模塊的編譯工作(configure、build)。這個過程,需要nodejs的頭文件以及靜態庫參與(后續稱庫文件)對C/C++項目編譯和鏈接。庫文件從哪里下載,會有一定邏輯進行處理,本文將從源碼入手進行分析。

編寫簡單的原生模塊

為了方便進行分析,我們首先創建一個原生模塊(關於如何編寫原生模塊的細節不再本文討論)。

hello_world.cc

#include <node.h>

void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(v8::String::NewFromUtf8(
      isolate, "world").ToLocalChecked());
}

void Initialize(v8::Local<v8::Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

binding.gyp

{
  "targets": [
    {
      "target_name": "hello_world",
      "sources": [ "hello_world.cc" ]
    }
  ]
}

index.js

const binding = require('./build/Release/hello_world');

console.log(binding.hello());

package.json

...  
  "scripts": {
    "build": "node-gyp configure && node-gyp build",
    "run:demo": "node index.js"
  },
...

整體結構

按照如下命令依次運行:

$ npm run build
// 使用node-gyp配置並構建
$ npm run run:demo
// 運行Demo

輸出如下:

D:\Projects\node-addon-demo>npm run run:demo

> node-addon-demo@1.0.0 run:demo
> node index.js

world

從源碼分析node-gyp下載庫文件的路徑

首先要直接給出一個結論,庫文件並不是每次都要從網絡上下載,庫文件下載后會緩存在本地一個目錄,在Windows上為C:\Users\用戶\AppData\Local\node-gyp\Cache中,並按照nodejs的版本進行存儲:

本人電腦安裝的node版本為14.15.0,且曾經已經緩存了對應的庫文件。

為了便於分析,我們首先刪除該緩存文件,並且在原有的npm命令加上--verbose,輸出更加詳細的日志:

$ npm run build --verbose

於是,我們可以從眾多的輸出中,看到一個關鍵信息:

從日志中可以看出,node-gyp在構建過程中,會創建緩存目錄,然后從指定URL下載指定版本的headers文件。

我們利用GrepWin(一款Windows下超好用的文本內容搜索工具,官網),在node-gyp目錄中搜索created nodedir這個關鍵詞,因為可以看到gyp http GET上面出現了這個關鍵詞。那么現在有一個新的問題,node-gyp目錄在哪兒?其實,從上面的日志往上查看,能夠找到:

這里是調用的我們全局安裝的npm依賴的node-gyp,於是我們定位到node-gyp所在目錄進行搜索:

進入該文件,我們找到:

找到關鍵詞搜索后,繼續往后續代碼查閱,能夠看到一個download函數的調用,入參最后一位是url,此時已經是成型的url,所以接下來我們需要確定,release.tarballUrl這個值,究竟是什么時候確定的。

tarballUrl如何得到

繼續向上翻閱代碼,能夠在入口處看到這個release是如何生成的:

進入代碼后,能夠找到一段核心的構建:

通過上述代碼流程,我們總結出來,tarballUrl的baseUrl取決於是否存在overrideDistUrl,若存在,則直接使用;否則使用默認URL:https://nodejs.org/dist

再查看overrideDistUrl的傳入點:

也就是說,gyp對象的opts屬性存在dist-urldisturl時,就會使用該值作為庫文件下載的baseUrl。

如何構建gyp.opts

首先檢查該函數的調用點:

發現configure和install.js都使用了該函數,且都是入口處進行的調用的:

configure.js

function configure (gyp, argv, callback) {
  var python
  var buildDir = path.resolve('build')
  var configNames = ['config.gypi', 'common.gypi']
  var configs = []
  var nodeDir
  var release = processRelease(argv, gyp, process.version, process.release)
  ......
}
module.exports = configure

install.js

function install (fs, gyp, argv, callback) {
  var release = processRelease(argv, gyp, process.version, process.release)
  ......
}
module.exports = function (gyp, argv, callback) {
  return install(fs, gyp, argv, callback)
}

可以看到confiigure.js和install.js都作為函數形式導出,也就是說,gyp這個對象是在這兩個模塊在被導入並以函數形式調用時被傳入的。那么接下來我們需要看這兩個模塊在何處使用的。

在上文我們查看當前執行的node-gyp目錄的時候,我們就看到過:

gyp verb cli [
gyp verb cli   'D:\\Programs\\nodejs\\node.exe',
gyp verb cli   'D:\\Programs\\nodejs\\global_modules\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js',
gyp verb cli   'configure'
gyp verb cli ]

入口函數是:node-gyp根目錄/bin/node-gyp.js。所以,我們將node-gyp以項目的形式添加到IDEA中,嘗試以相同的形式調用這些命令,通過開啟DEBUG模式,來一探究竟。

bin/node-gyp.js中的最下方進行了一個名為run的函數調用:

// bin/node-gyp.js

// ......
// 還有很多省略的代碼......

// start running the given commands!
run()

根據注釋可以,run()執行所提供的命令。翻閱該函數:

總體分為兩步:

  1. 從對象prog的todo這個數組中取出首個command命令對象,不存在判定為所有命令執行完成。
  2. 從對象prog的命令數組(commands)中找到對應命令名稱(command.name),通過代碼可知,該命令實際上對應一個函數。傳入參數(command.args)完成該函數的調用。

那么這個prog是什么呢?通過向上閱讀代碼,可以知道來自於上層目錄提供的模塊:

而上層所指代的模塊是通過package.json的main字段可知是lib/node-gyp.js

// 根目錄下的package.json
"main": "./lib/node-gyp.js",

進入該文件的gyp函數,返回的是類Gyp的實例,而Gyp實例的構造過程如下:

  1. 使用self變量指代Gyp實例,並創建devDir和commands字段。
  2. 遍歷上方的commands字符串數組,給self(也就是Gyp實例)的commands屬性中,逐步添加對應命令名稱的函數,函數的實現是:require和command同名的js模塊,這些模塊又本身是以函數形式導出的,最終是調用對應模塊函數。舉例說明:當遍歷到command為configure的時候,就是如下的形式:
  self.commands['configure'] = function (argv, callback) {
    log.verbose('command', 'configure', argv)
    return require('./configure')(self, argv, callback)
  }

那么在進行node-gyp configure時的調用棧就如下:

執行node-gyp configure:
=> run()
...
...
=> gyp.commands['configure'](argv, cb);
=> require('./configure')(self, argv, cb); // self就是Gyp實例

前文我們已經知道了configure.js這個模塊導出的就是一個函數:

// configure.js
function configure (gyp, argv, callback) {
  var python
  var buildDir = path.resolve('build')
  var configNames = ['config.gypi', 'common.gypi']
  var configs = []
  var nodeDir
  // 這個gyp,就是入參gyp,也就是上面的gyp實例
  var release = processRelease(argv, gyp, process.version, process.release)
  ... ...
}
... ...
module.exports = configure

所以,我們終於知道processRelease的入參的gyp,就是上面的gyp實例。那么gyp實例中的opts屬性,是哪兒來的呢?使用IDEA的Debug進行斷點調式,調試bin/node-gyp.js

可以看到,在執行parseArgv這個函數前,gyp實例里面還不存在opts屬性,而執行后,又在使用opts屬性的devdir。也就是說,parseArgv這個函數一定構建了opts,接下來我們重點分析這個函數。

入口的argv就是我們的運行時入參:

"dev": "node ./bin/node-gyp.js configure"

首先會經過nopt函數,看樣子,是對命令行參數以及短命令的處理:

然后是該函數其他的部分:

主要分為兩個部分:

  1. 對argv的解析
  2. 對環境變量的解析

對argv的解析不涉及設置opts屬性,我們重點看對環境變量的解析:

  // support for inheriting config env variables from npm
  var npmConfigPrefix = 'npm_config_'
  Object.keys(process.env).forEach(function (name) {
    if (name.indexOf(npmConfigPrefix) !== 0) {
      return
    }
    var val = process.env[name]
    if (name === npmConfigPrefix + 'loglevel') {
      log.level = val
    } else {
      // add the user-defined options to the config
      name = name.substring(npmConfigPrefix.length)
      // gyp@741b7f1 enters an infinite loop when it encounters
      // zero-length options so ensure those don't get through.
      if (name) {
        this.opts[name] = val
      }
    }
  }, this)

處理流程為:

  1. 判斷環境變量的名稱(name),如果不是npm_config_開頭,則跳過該次處理,否則進入下一步。
  2. 如果變量名是npm_config_loglevel(npm的日志等級變量),則使用該日志等級作為node-gyp在使用npm時候的日志變量(這是對日志等級的特殊處理)。
  3. 否則(一般處理),截斷該變量的名,例如name = 'npm_config_my_key',則得到my_key,設置到opts中:opts['my_key'] = 變量值

至此,我們已經知道了,opts屬性的值來源於上述的解析。

那么,回到我們一開始的目的,我們知道了要實現從指定的地方下載node的庫文件,只要opts里面存在dist-url或是disturl即可。有些讀者可能會說,那這樣就行了呀:

實際上,並不行:

解析結束后,會發現,gyp.opts中是不存在dist-url字段的,只有dist_url。這一切的緣由,都是因為,npm在處理環境變量的時候,會將-替換為下划線_config | npm Docs (npmjs.com))。

好在,node-gyp還能夠處理opts中的disturl字段。所以我們只需要在使用npm來使用node-gyp的時候,加入參數--disturl。現在,讓我們回到我們一開始的node-addon-demo,添加設置變量的參數:

  "scripts": {
    "build": "node-gyp configure && node-gyp build",
    "build:custom": "npm run build --verbose --disturl=this_is_my_custom_url",
    "run:demo": "node index.js"
  },

上述build:custom就是我們新加的配置,通過運行,果然,加載的是我們制定的url:

gyp verb created nodedir C:\Users\w4ngzhen\AppData\Local\node-gyp\Cache\14.15.0
// 這里報錯忽略,因為使用的是一個無效的url: 'this_is_my_custom_url'
// 主要是為了驗證確實是改變了
gyp http GET this_is_my_custom_url/v14.15.0/node-v14.15.0-headers.tar.gz
gyp WARN install got an error, rolling back install
gyp verb command remove [ '14.15.0' ]

node-gyp的直接使用和npm使用的區別

那么,有的細心的讀者可能會說,明明這里通過npm使用的時候會轉為下划線,那在node-gyp的官方github,說是可以使用dist-url這個參數呢?。

nodejs/node-gyp: Node.js native addon build tool (github.com)

實際上,官方文檔給出的參數,需要你直接使用node-gyp方式進行設置,也就是說,--dist-url這個參數必須緊跟node-gyp的命令:

node-gyp configure --dist-url=xxx

像是上面的npm run ${使用node-gyp的腳本名} --dist-url=xxx,這個dist-url是作為npm的參數來被識別,而非node-gyp。所以,對於demo,我們還可以如下:

  "scripts": {
    "build": "node-gyp configure --dist-url=this_is_my_custom_url && node-gyp build --dist-url=this_is_my_custom_url",
    "build:custom": "npm run build --verbose",
    "run:demo": "node index.js"
  },

注意,這一次,我把--dist-url是放在和node-gyp命令的參數的。但是,我們知道有些npm包,內部就直接使用node-gyp進行配置編譯的操作,這個過程沒法通過--dist-url緊跟node-gyp命令方式,所以只能在例如.npmrc文件中配置兼容的不會被下划線處理的disturl

總結

要想讓node-gyp下載node庫文件的時候,能夠走指定的鏡像,可以通過配置--dist-url或是--disturl的方式,但配置dist-url形式參數只能是參數緊跟node-gyp的形式:

node-gyp configure --dist-url=xxx

不能是如下的形式:

// 你的package.json scripts字段
"build": "node-gyp configure"
// 然后在命令行調用
npm run build --dist-url=xxx // 

因為此時--dist-url參數是npm的參數,且會被處理為npm_config_dist_url下划線形式,進而在gyp.opts只有dist_url屬性。

所以,最安全的方式是使用disturl參數:

情況1:

node-gyp configure --disturl=xxx

情況2:

// 你的package.json scripts字段
"build": "node-gyp configure"
// 然后在命令行調用
npm run build --disturl=xxx

情況1下,disturl是作為node-gyp的參數進行解析,能夠被設置到opts中。

情況2,disturl是作為npm的參數被加入到npm環境變量:npm_config_disturl,此時,node-gyp解析process.env的時候,也能解析到disturl進而設置到opts。


免責聲明!

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



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