Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

刨根问底之node-gyp #5

Open
tsy77 opened this issue Jul 1, 2018 · 4 comments
Open

刨根问底之node-gyp #5

tsy77 opened this issue Jul 1, 2018 · 4 comments

Comments

@tsy77
Copy link
Owner

tsy77 commented Jul 1, 2018

在我们写node addon时,需要使用node-gyp命令行工具,大部分同学会用configue生成配置文件,然后使用build进行构建。但是node-gyp到底是什么?底层有什么呢?下面我们来刨根问底。

本文的线索是自底向上的讲解node-gyp的各层次依赖,主要有以下几个部分:

1. make
2. make install
3. cmake
4. gyp
5. node-gyp

层次结构如下图所示:

make

从源文件到可执行文件叫做编译(包括预编译、编译、链接),而make作为构建工具掌握着编译的过程,也就是如何去编译、文件编译的顺序等。

make是最常用的构建工具,针对用户制定的构建规则(makefile)去执行响应的任务。make会根据构建规则去查找依赖,决定编译顺序等。大致了解可参考Make 命令教程

Makefile(makefile)中定义了make的构建规则,当然也可以自己指定规则文件。例如:

$ make -f rules.txt
# 或者
$ make --file=rules.txt

Makefile由一条条的规则组成,每条规则由target(目标)、source(前置条件/依赖)、command(指令)三者组成。

形式如下:

<target> : <prerequisites> 
[tab]  <commands>

make target时,主要做了以下几件事:

1.检查目标是否存在
2.如果不存在目标
	· 检查目标的依赖是否存在
	· 不存在则调用`make source`;存在并且没有变化(修改时间戳小于target),不操作
	· 执行target中的command指令
2.如果存在目标
	· 检查依赖是否发生变化
	· 没有变化则不需要执行,有变化则执行`make source`后执行command

以编译一个C++文件的规则为例:

hellomake: hellomake.c hellofunc.c
     gcc -o hellomake hellomake.c hellofunc.c -I.

当我们执行make hellomake,会使用gcc编译器编译产出hellomake。如果make不带有参数,则执行makefile中的第一条指令。

make也允许我们定义一些纯指令(伪指令)去执行一些操作,相当于把上面的target写成指令名称,只不过在command中不生成文件,所以每次执行该规则时都会执行command。为了和真实的目标文件做区分,make中使用了.PHONY关键字,关键字.PHONY可以解决这问题,告诉make该目标是“假的”(磁盘上其实没有这个目标文件)。例如

.PHONY: clean
clean:
        rm *.o temp

由于makefile目标只能写一个,所以我们可以使用all来将多个目标组合起来。例如:

all: executable1 executable2

一般情况下可以把all放在makefile的第一行,这样不带参数执行make就会找到all。

make install

make install用来安装文件,它从Makefile中读取指令,安装到系统目录中。

cmake

上面提到了make,似乎已经够了,如果我是一个开发者,我定义了makefile,让使用者执行make编译就好了。但是不同平台的编译器、动态链接库的路径都有可能不同,如果想让你的软件能够跨平台编译、运行,必须要保证能够在不同平台编译。如果使用上面的Make工具,就得为每一种标准写一次Makefile,这是很繁琐并且容易出错的地方。

cmake的出现就是为了解决上述问题,它首先允许开发者编写一种平台无关的 CMakeList.txt文件来定制整个编译流程,cmake会根据操作系统选择不同编译器,当然也可以在CMakeList.txt中去指定,执行cmake时会目标用户的平台和自定义的配置生成所需的Makefile或工程文件,如Unix的Makefile、Windows的Visual Studio。

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。

在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

1.编写 CMake 配置文件 CMakeLists.txt 。
2.执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile。其中,PATH是CMakeLists.txt 所在的目录。
3.使用 make 命令进行编译。

CMakeList.txt中由面向过程的一条条指令组成,例如:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo3)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 添加 math 子目录
add_subdirectory(math)
# 指定生成目标 
add_executable(Demo main.cc)
# 添加链接库
target_link_libraries(Demo MathFunctions)

具体可参考cmake文档

GYP

Gyp是一个类似CMake的项目生成工具, 用于管理你的源代码, 在google code主页上唯一的一句slogan是”GYP can Generate Your Projects.”。GYP是由 Chromium 团队开发的跨平台自动化项目构建工具,Chromium便是通过GYP进行项目构建管理。

首先看GYP与cmake类似,那为什要有GYP呢?GYP和cmake有哪些相同点、不同点呢?

GYP vs cmake

相同点:

支持跨平台项目工程文件输出,Windows 平台默认是 Visual Studio,Linux 平台默认是 Makefile,Mac 平台默认是 Xcode,这个功能 CMake 也同样支持,只是缺少了 Xcode。

不同点:

配置文件形式不同,GYP的配置文件更像一个“配置文件”,而Cmake的上述所言更像一个面向过程的一个脚本,也就是说在项目设置的层次上进行抽象;同时GYP支持交叉编译。

具体比较可参考GYP vs. CMake

GYP配置

GYP的配置文件以.gyp结尾,一个典型的.gyp文件如下所示:

{
    'variables': {
      .
      .
      .
    },
    'includes': [
      '../build/common.gypi',
    ],
    'target_defaults': {
      .
      .
      .
    },
    'targets': [
      {
        'target_name': 'target_1',
          .
          .
          .
      },
      {
        'target_name': 'target_2',
          .
          .
          .
      },
    ],
    'conditions': [
      ['OS=="linux"', {
        'targets': [
          {
            'target_name': 'linux_target_3',
              .
              .
              .
          },
        ],
      }],
      ['OS=="win"', {
        'targets': [
          {
            'target_name': 'windows_target_4',
              .
              .
              .
          },
        ],
      }, { # OS != "win"
        'targets': [
          {
            'target_name': 'non_windows_target_5',
              .
              .
              .
          },
      }],
    ],
  }

variables : 定义可以在文件其他地方访问的变量;

includes : 将要被引入到该文件中的文件列表,通常是以.gypi结尾的文件

target_defaults : 将作用域所有目标的默认配置;

targets: 构建的目标列表,每个target中包含构建此目标的所有配置;

conditions: 条件列表,会根据不同条件选择不同的配置项。在最顶级的配置中,通常是平台特定的目标配置。

具体可参考GYP文档

node-gyp

node-gyp是一个跨平台的命令行工具,目的是编译node addon模块。

常用的命令有configurebuildconfigure 原理就是利用gyp生成不同的编译配置文件,build则根据不同平台、不同构建配置进行编译。

configure

我们分步骤看下configure的代码:

findPython(python, function (err, found) {
    if (err) {
      callback(err)
    } else {
      python = found
      getNodeDir()
    }
})

由于GYP是python写的,所以这里首先找当前系统下的python,内部利用的是which这个第三方库。

function getNodeDir () {

    // 'python' should be set by now
    process.env.PYTHON = python

    if (gyp.opts.nodedir) {
      // --nodedir was specified. use that for the dev files
      nodeDir = gyp.opts.nodedir.replace(/^~/, osenv.home())

      log.verbose('get node dir', 'compiling against specified --nodedir dev files: %s', nodeDir)
      createBuildDir()

    } else {
      gyp.commands.install([ release.version ], function (err, version) {
        if (err) return callback(err)
        log.verbose('get node dir', 'target node version installed:', release.versionDir)
        nodeDir = path.resolve(gyp.devDir, release.versionDir)
        createBuildDir()
      })
    }
  }

找到node所在目录,如果没有,则下载node压缩包并解压。

function createBuildDir () {
    log.verbose('build dir', 'attempting to create "build" dir: %s', buildDir)
    mkdirp(buildDir, function (err, isNew) {
      if (err) return callback(err)
      log.verbose('build dir', '"build" dir needed to be created?', isNew)
      if (win && (!gyp.opts.msvs_version || gyp.opts.msvs_version === '2017')) {
        findVS2017(function (err, vsSetup) {
          if (err) {
            log.verbose('Not using VS2017:', err.message)
            createConfigFile()
          } else {
            createConfigFile(null, vsSetup)
          }
        })
      } else {
        createConfigFile()
      }
    })
  }

创建build目录,这里区分了是否有vs,查找vs的方法是打开powershell(windows),试图打开vs。

function createConfigFile (err, vsSetup) {
    if (err) return callback(err)

    var configFilename = 'config.gypi'
    var configPath = path.resolve(buildDir, configFilename)

    if (vsSetup) {
      // GYP doesn't (yet) have support for VS2017, so we force it to VS2015
      // to avoid pulling a floating patch that has not landed upstream.
      // Ref: https://chromium-review.googlesource.com/#/c/433540/
      gyp.opts.msvs_version = '2015'
      process.env['GYP_MSVS_VERSION'] = 2015
      process.env['GYP_MSVS_OVERRIDE_PATH'] = vsSetup.path
      defaults['msbuild_toolset'] = 'v141'
      defaults['msvs_windows_target_platform_version'] = vsSetup.sdk
      variables['msbuild_path'] = path.join(vsSetup.path, 'MSBuild', '15.0',
                                            'Bin', 'MSBuild.exe')
    }

    // loop through the rest of the opts and add the unknown ones as variables.
    // this allows for module-specific configure flags like:
    //
    //   $ node-gyp configure --shared-libxml2
    Object.keys(gyp.opts).forEach(function (opt) {
      if (opt === 'argv') return
      if (opt in gyp.configDefs) return
      variables[opt.replace(/-/g, '_')] = gyp.opts[opt]
    })

    configs.push(configPath)
    fs.writeFile(configPath, [prefix, json, ''].join('\n'), findConfigs)
}

这里创建config.gypi文件,主要包含target_defaultsvariables

// config = ['config.gypi']
  function runGyp (err) {
    if (err) return callback(err)

    if (!~argv.indexOf('-f') && !~argv.indexOf('--format')) {
      if (win) {
        log.verbose('gyp', 'gyp format was not specified; forcing "msvs"')
        // force the 'make' target for non-Windows
        argv.push('-f', 'msvs')
      } else {
        log.verbose('gyp', 'gyp format was not specified; forcing "make"')
        // force the 'make' target for non-Windows
        argv.push('-f', 'make')
      }
    }

    if (win && !hasMsvsVersion()) {
      if ('msvs_version' in gyp.opts) {
        argv.push('-G', 'msvs_version=' + gyp.opts.msvs_version)
      } else {
        argv.push('-G', 'msvs_version=auto')
      }
    }

    // include all the ".gypi" files that were found
    configs.forEach(function (config) {
      argv.push('-I', config)
    })

    // For AIX and z/OS we need to set up the path to the exports file
    // which contains the symbols needed for linking. 
    var node_exp_file = undefined
    if (process.platform === 'aix' || process.platform === 'os390') {
      var ext = process.platform === 'aix' ? 'exp' : 'x'
      var node_root_dir = findNodeDirectory()
      var candidates = undefined 
      if (process.platform === 'aix') {
        candidates = ['include/node/node',
                      'out/Release/node',
                      'out/Debug/node',
                      'node'
                     ].map(function(file) {
                       return file + '.' + ext
                     })
      } else {
        candidates = ['out/Release/obj.target/libnode',
                      'out/Debug/obj.target/libnode',
                      'lib/libnode'
                     ].map(function(file) {
                       return file + '.' + ext
                     })
      }
      var logprefix = 'find exports file'
      node_exp_file = findAccessibleSync(logprefix, node_root_dir, candidates)
      if (node_exp_file !== undefined) {
        log.verbose(logprefix, 'Found exports file: %s', node_exp_file)
      } else {
        var msg = msgFormat('Could not find node.%s file in %s', ext, node_root_dir)
        log.error(logprefix, 'Could not find exports file')
        return callback(new Error(msg))
      }
    }

    // this logic ported from the old `gyp_addon` python file
    var gyp_script = path.resolve(__dirname, '..', 'gyp', 'gyp_main.py')
    var addon_gypi = path.resolve(__dirname, '..', 'addon.gypi')
    var common_gypi = path.resolve(nodeDir, 'include/node/common.gypi')
    fs.stat(common_gypi, function (err, stat) {
      if (err)
        common_gypi = path.resolve(nodeDir, 'common.gypi')

      var output_dir = 'build'
      if (win) {
        // Windows expects an absolute path
        output_dir = buildDir
      }
      var nodeGypDir = path.resolve(__dirname, '..')
      var nodeLibFile = path.join(nodeDir,
        !gyp.opts.nodedir ? '<(target_arch)' : '$(Configuration)',
        release.name + '.lib')

      argv.push('-I', addon_gypi)
      argv.push('-I', common_gypi)
      argv.push('-Dlibrary=shared_library')
      argv.push('-Dvisibility=default')
      argv.push('-Dnode_root_dir=' + nodeDir)
      if (process.platform === 'aix' || process.platform === 'os390') {
        argv.push('-Dnode_exp_file=' + node_exp_file)
      }
      argv.push('-Dnode_gyp_dir=' + nodeGypDir)
      argv.push('-Dnode_lib_file=' + nodeLibFile)
      argv.push('-Dmodule_root_dir=' + process.cwd())
      argv.push('-Dnode_engine=' +
        (gyp.opts.node_engine || process.jsEngine || 'v8'))
      argv.push('--depth=.')
      argv.push('--no-parallel')

      // tell gyp to write the Makefile/Solution files into output_dir
      argv.push('--generator-output', output_dir)

      // tell make to write its output into the same dir
      argv.push('-Goutput_dir=.')

      // enforce use of the "binding.gyp" file
      argv.unshift('binding.gyp')

      // execute `gyp` from the current target nodedir
      argv.unshift(gyp_script)

      // make sure python uses files that came with this particular node package
      var pypath = [path.join(__dirname, '..', 'gyp', 'pylib')]
      if (process.env.PYTHONPATH) {
        pypath.push(process.env.PYTHONPATH)
      }
      process.env.PYTHONPATH = pypath.join(win ? ';' : ':')

      var cp = gyp.spawn(python, argv)
      cp.on('exit', onCpExit)
    })
}

这里主要是区分了不同平台,给GYP命令加入各种参数,其中-I代表include,最后执行gyp脚本生成构建配置文件,比如unix下生成makefile。

build

build比较简单,言简意赅就是就是区分不同平台,收集不同参数,利用不同编译工具进行编译。

command = win ? 'msbuild' : makeCommand

区分编译工具。

function loadConfigGypi () {
    fs.readFile(configPath, 'utf8', function (err, data) {
      if (err) {
        if (err.code == 'ENOENT') {
          callback(new Error('You must run `node-gyp configure` first!'))
        } else {
          callback(err)
        }
        return
      }
      config = JSON.parse(data.replace(/\#.+\n/, ''))

      // get the 'arch', 'buildType', and 'nodeDir' vars from the config
      buildType = config.target_defaults.default_configuration
      arch = config.variables.target_arch
      nodeDir = config.variables.nodedir

      if ('debug' in gyp.opts) {
        buildType = gyp.opts.debug ? 'Debug' : 'Release'
      }
      if (!buildType) {
        buildType = 'Release'
      }

      log.verbose('build type', buildType)
      log.verbose('architecture', arch)
      log.verbose('node dev dir', nodeDir)

      if (win) {
        findSolutionFile()
      } else {
        doWhich()
      }
    })
}

加载config.gypi,为构建收集一波参数。如果在windows下,收集build/*.sln


  function doBuild () {

    // Enable Verbose build
    var verbose = log.levels[log.level] <= log.levels.verbose
    if (!win && verbose) {
      argv.push('V=1')
    }
    if (win && !verbose) {
      argv.push('/clp:Verbosity=minimal')
    }

    if (win) {
      // Turn off the Microsoft logo on Windows
      argv.push('/nologo')
    }

    // Specify the build type, Release by default
    if (win) {
      var archLower = arch.toLowerCase()
      var p = archLower === 'x64' ? 'x64' :
              (archLower === 'arm' ? 'ARM' : 'Win32')
      argv.push('/p:Configuration=' + buildType + ';Platform=' + p)
      if (jobs) {
        var j = parseInt(jobs, 10)
        if (!isNaN(j) && j > 0) {
          argv.push('/m:' + j)
        } else if (jobs.toUpperCase() === 'MAX') {
          argv.push('/m:' + require('os').cpus().length)
        }
      }
    } else {
      argv.push('BUILDTYPE=' + buildType)
      // Invoke the Makefile in the 'build' dir.
      argv.push('-C')
      argv.push('build')
      if (jobs) {
        var j = parseInt(jobs, 10)
        if (!isNaN(j) && j > 0) {
          argv.push('--jobs')
          argv.push(j)
        } else if (jobs.toUpperCase() === 'MAX') {
          argv.push('--jobs')
          argv.push(require('os').cpus().length)
        }
      }
    }

    if (win) {
      // did the user specify their own .sln file?
      var hasSln = argv.some(function (arg) {
        return path.extname(arg) == '.sln'
      })
      if (!hasSln) {
        argv.unshift(gyp.opts.solution || guessedSolution)
      }
    }

    var proc = gyp.spawn(command, argv)
    proc.on('exit', onExit)
}

执行编译命令。

@tsy77 tsy77 changed the title make、cmake 刨根问底之node-gyp Jul 2, 2018
@BalconyFarmer
Copy link

666

@csjjzhou
Copy link

大写的赞!

@JasonJarvan
Copy link

太赞了!

@lippzhang
Copy link

学到了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants